From 935e17a222fe08e64fdd3e6a47ee22215ca8486b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Aug 2026 16:03:03 +0200 Subject: [PATCH] Refactor Frontier Silicon tests (#180264) --- tests/components/frontier_silicon/__init__.py | 12 + tests/components/frontier_silicon/conftest.py | 235 +++++------------- .../frontier_silicon/test_config_flow.py | 195 +++++++-------- .../frontier_silicon/test_media_player.py | 179 ++++++------- 4 files changed, 260 insertions(+), 361 deletions(-) diff --git a/tests/components/frontier_silicon/__init__.py b/tests/components/frontier_silicon/__init__.py index 6a039dc29acd..f38a31c04c73 100644 --- a/tests/components/frontier_silicon/__init__.py +++ b/tests/components/frontier_silicon/__init__.py @@ -1 +1,13 @@ """Tests for the Frontier Silicon integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py index 33d99e28e339..63701ca2eb39 100644 --- a/tests/components/frontier_silicon/conftest.py +++ b/tests/components/frontier_silicon/conftest.py @@ -3,8 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from afsapi import Equaliser, FSConnectionError, PlayCaps, PlayerMode, Preset -from afsapi.nodes import PresetsListItem +from afsapi import Equaliser, PlayCaps, PlayerMode, PlayState, Preset import pytest from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN @@ -13,140 +12,73 @@ from homeassistant.const import CONF_PIN from tests.common import MockConfigEntry -class FakeAFSAPIDevice: - """A fake Frontier Silicon Device.""" - - # registry of fake AFSAPI devices - afsapi_device_map = {} - - def __new__(cls, webfsapi_endpoint: str, pin: str | int, timeout: int = 2): - """Create or reuse a fake device for the endpoint.""" - - if webfsapi_endpoint not in cls.afsapi_device_map: - cls.afsapi_device_map[webfsapi_endpoint] = super().__new__(cls) - return cls.afsapi_device_map[webfsapi_endpoint] - - def __init__( - self, webfsapi_endpoint: str, pin: str | int, timeout: int = 2 - ) -> None: - """Constructor.""" - if not hasattr(self, "init_done"): - self.webfsapi_endpoint = webfsapi_endpoint - self.reset() - self.init_done = True - - def reset(self): - """Reset the state of the device, ready for the next test.""" - self.fail_get_power = False - self.has_power = False - - async def get_radio_id(self) -> str: - """Mock get_radio_id AFSAPI function.""" - return "FakeID" - - async def get_power(self) -> bool: - """Mock get_power AFSAPI function.""" - if self.fail_get_power: - raise FSConnectionError - return self.has_power - - async def get_play_name(self) -> str: - """Mock get_play_name AFSAPI function.""" - return "Something Playing" - - async def get_play_text(self) -> str: - """Mock get_play_text AFSAPI function.""" - return "Something Playing Extra Text" - - async def get_play_artist(self) -> str: - """Mock get_play_artist AFSAPI function.""" - return "Artist Name" - - async def get_play_album(self) -> str: - """Mock get_play_album AFSAPI function.""" - return "Album Name" - - async def get_play_status(self) -> int: - """Mock get_play_status AFSAPI function.""" - return 0 - - async def get_mode(self) -> PlayerMode: - """Mock get_mode AFSAPI function.""" - available_modes = await self.get_modes() - return available_modes[0] - - async def get_mute(self) -> bool: - """Mock get_mute AFSAPI function.""" - return False - - async def get_volume(self) -> int: - """Mock get_volume AFSAPI function.""" - return 3 - - async def get_play_graphic(self) -> str: - """Mock get_play_graphic AFSAPI function.""" - return "https://1.1.1.1/graphic_url" - - async def get_modes(self) -> list[PlayerMode]: - """Mock get_modes AFSAPI function.""" - valid_modes = [(0, {"id": "mocked_mode0", "label": "MockedMode"})] - - return [ - PlayerMode( - id=v["id"], - key=int(k), - label=v.get("label"), - selectable=v.get("selectable"), - streamable=v.get("streamable"), - modetype=v.get("modeType"), - ) - for k, v in valid_modes - ] - - async def get_play_caps(self) -> PlayCaps | None: - """Mock get_play_caps AFSAPI function.""" - return PlayCaps(0) - - async def get_presets(self) -> list[Preset]: - """Mock get_presets AFSAPI function.""" - - def _to_preset( - key: str, - preset_fields: PresetsListItem, - ) -> Preset: - """Internal helper function to convert data to Preset.""" - return Preset(int(key), preset_fields.get("type"), preset_fields["name"]) - - presets_data = [(0, {"type": "mocked_eqpreset_type0", "name": "MockedPreset"})] - - return [_to_preset(key, preset_fields) for key, preset_fields in presets_data] - - async def get_eq_preset(self) -> Equaliser: - """Mock get_eq_preset AFSAPI function.""" - available_equalisers = await self.get_equalisers() - return available_equalisers[0] - - async def get_equalisers(self) -> list[Equaliser]: - """Mock get_equalisers AFSAPI function.""" - equalisers_data = [(0, {"label": "MockedEq"})] - return [ - Equaliser(key=int(key), label=eqinfo["label"]) - for key, eqinfo in equalisers_data - ] - - async def get_volume_steps(self) -> int: - """Mock get_volume_steps AFSAPI function.""" - return 2 +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.frontier_silicon.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry @pytest.fixture -def fake_afsapi_dev(config_entry: MockConfigEntry): - """Return a test FakeAFSAPIDevice, creating it for an endpoint if needed.""" - webfsapi_endpoint = config_entry.data[CONF_WEBFSAPI_URL] - pin = config_entry.data[CONF_PIN] - fake_dev = FakeAFSAPIDevice(webfsapi_endpoint, pin) - yield fake_dev - fake_dev.reset() +def mock_afsapi() -> Generator[AsyncMock]: + """Mock a Frontier Silicon AFSAPI client.""" + with ( + patch( + "homeassistant.components.frontier_silicon.AFSAPI", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.frontier_silicon.config_flow.AFSAPI", + new=mock_client, + ), + ): + client = mock_client.return_value + client.webfsapi_endpoint = "http://1.1.1.1:80/webfsapi" + + # get_webfsapi_endpoint is a staticmethod on the class; expose it on the + # instance mock too so tests can configure it via the yielded client. + mock_client.get_webfsapi_endpoint.return_value = "http://1.1.1.1:80/webfsapi" + client.get_webfsapi_endpoint = mock_client.get_webfsapi_endpoint + client.get_friendly_name.return_value = "Name of the device" + client.get_radio_id.return_value = "mock_radio_id" + + client.get_power.return_value = False + client.get_play_status.return_value = PlayState.IDLE + client.get_play_name.return_value = "Something Playing" + client.get_play_text.return_value = "Something Playing Extra Text" + client.get_play_artist.return_value = "Artist Name" + client.get_play_album.return_value = "Album Name" + client.get_play_graphic.return_value = "https://1.1.1.1/graphic_url" + client.get_mute.return_value = False + client.get_volume.return_value = 3 + client.get_volume_steps.return_value = 2 + client.get_play_caps.return_value = PlayCaps(0) + + modes = [ + PlayerMode( + id="mocked_mode0", + key=0, + label="MockedMode", + selectable=True, + streamable=None, + modetype=None, + ) + ] + client.get_modes.return_value = modes + client.get_mode.return_value = modes[0] + + equalisers = [Equaliser(key=0, label="MockedEq")] + client.get_equalisers.return_value = equalisers + client.get_eq_preset.return_value = equalisers[0] + + client.get_presets.return_value = [ + Preset(0, "mocked_eqpreset_type0", "MockedPreset") + ] + + yield client @pytest.fixture @@ -154,42 +86,7 @@ def config_entry() -> MockConfigEntry: """Create a mock Frontier Silicon config entry.""" return MockConfigEntry( domain=DOMAIN, + title="Name of the device", unique_id="mock_radio_id", data={CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", CONF_PIN: "1234"}, ) - - -@pytest.fixture(autouse=True) -def mock_valid_device_url() -> Generator[None]: - """Return a valid webfsapi endpoint.""" - with patch( - "afsapi.AFSAPI.get_webfsapi_endpoint", - return_value="http://1.1.1.1:80/webfsapi", - ): - yield - - -@pytest.fixture(autouse=True) -def mock_valid_pin() -> Generator[None]: - """Make get_friendly_name return a value, indicating a valid pin.""" - with patch( - "afsapi.AFSAPI.get_friendly_name", - return_value="Name of the device", - ): - yield - - -@pytest.fixture(autouse=True) -def mock_radio_id() -> Generator[None]: - """Return a valid radio_id.""" - with patch("afsapi.AFSAPI.get_radio_id", return_value="mock_radio_id"): - yield - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.frontier_silicon.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py index b8f60b2136ab..763db14086a3 100644 --- a/tests/components/frontier_silicon/test_config_flow.py +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Frontier Silicon config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from afsapi import FSConnectionError, FSNotImplementedError, InvalidPinError import pytest @@ -18,7 +18,7 @@ from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from tests.common import MockConfigEntry -pytestmark = pytest.mark.usefixtures("mock_setup_entry") +pytestmark = pytest.mark.usefixtures("mock_afsapi", "mock_setup_entry") MOCK_DISCOVERY = SsdpServiceInfo( @@ -46,6 +46,7 @@ INVALID_MOCK_DISCOVERY = SsdpServiceInfo( ) async def test_form_default_pin( hass: HomeAssistant, + mock_afsapi: AsyncMock, mock_setup_entry: AsyncMock, radio_id_return_value: str | None, radio_id_side_effect: Exception | None, @@ -58,15 +59,12 @@ async def test_form_default_pin( assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_radio_id", - return_value=radio_id_return_value, - side_effect=radio_id_side_effect, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, - ) + mock_afsapi.get_radio_id.return_value = radio_id_return_value + mock_afsapi.get_radio_id.side_effect = radio_id_side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY @@ -84,6 +82,7 @@ async def test_form_default_pin( ) async def test_form_nondefault_pin( hass: HomeAssistant, + mock_afsapi: AsyncMock, mock_setup_entry: AsyncMock, radio_id_return_value: str | None, radio_id_side_effect: Exception | None, @@ -96,29 +95,24 @@ async def test_form_nondefault_pin( assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_friendly_name", - side_effect=InvalidPinError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, - ) - await hass.async_block_till_done() + mock_afsapi.get_friendly_name.side_effect = InvalidPinError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_radio_id", - return_value=radio_id_return_value, - side_effect=radio_id_side_effect, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_PIN: "4321"}, - ) + mock_afsapi.get_friendly_name.side_effect = None + mock_afsapi.get_radio_id.return_value = radio_id_return_value + mock_afsapi.get_radio_id.side_effect = radio_id_side_effect + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY @@ -140,6 +134,7 @@ async def test_form_nondefault_pin( ) async def test_form_nondefault_pin_invalid( hass: HomeAssistant, + mock_afsapi: AsyncMock, friendly_name_error: Exception, result_error: str, mock_setup_entry: AsyncMock, @@ -152,34 +147,29 @@ async def test_form_nondefault_pin_invalid( assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_friendly_name", - side_effect=InvalidPinError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, - ) - await hass.async_block_till_done() + mock_afsapi.get_friendly_name.side_effect = InvalidPinError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_friendly_name", - side_effect=friendly_name_error, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_PIN: "4321"}, - ) - await hass.async_block_till_done() + mock_afsapi.get_friendly_name.side_effect = friendly_name_error + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() assert result3["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result3["errors"] == {"base": result_error} + mock_afsapi.get_friendly_name.side_effect = None result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_PIN: "4321"}, @@ -204,6 +194,7 @@ async def test_form_nondefault_pin_invalid( ) async def test_invalid_device_url( hass: HomeAssistant, + mock_afsapi: AsyncMock, webfsapi_endpoint_error: Exception, result_error: str, mock_setup_entry: AsyncMock, @@ -216,20 +207,18 @@ async def test_invalid_device_url( assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_webfsapi_endpoint", - side_effect=webfsapi_endpoint_error, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, - ) - await hass.async_block_till_done() + mock_afsapi.get_webfsapi_endpoint.side_effect = webfsapi_endpoint_error + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": result_error} + mock_afsapi.get_webfsapi_endpoint.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, @@ -247,6 +236,7 @@ async def test_invalid_device_url( async def test_user_already_configured_without_unique_id( hass: HomeAssistant, + mock_afsapi: AsyncMock, ) -> None: """Test manual setup aborts when an entry with the same endpoint already exists.""" entry = MockConfigEntry( @@ -256,17 +246,14 @@ async def test_user_already_configured_without_unique_id( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_radio_id", - side_effect=FSNotImplementedError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, - ) + mock_afsapi.get_radio_id.side_effect = FSNotImplementedError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -278,21 +265,19 @@ async def test_user_already_configured_without_unique_id( ) async def test_ssdp( hass: HomeAssistant, - mock_setup_entry: MockConfigEntry, + mock_afsapi: AsyncMock, + mock_setup_entry: AsyncMock, radio_id_return_value: str | None, radio_id_side_effect: Exception | None, ) -> None: """Test a device being discovered.""" - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_radio_id", - return_value=radio_id_return_value, - side_effect=radio_id_side_effect, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) + mock_afsapi.get_radio_id.return_value = radio_id_return_value + mock_afsapi.get_radio_id.side_effect = radio_id_side_effect + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -351,35 +336,32 @@ async def test_ssdp_already_configured( [(ValueError, "unknown"), (FSConnectionError, "cannot_connect")], ) async def test_ssdp_fail( - hass: HomeAssistant, webfsapi_endpoint_error: Exception, result_error: str + hass: HomeAssistant, + mock_afsapi: AsyncMock, + webfsapi_endpoint_error: Exception, + result_error: str, ) -> None: """Test a device being discovered but failing to reply.""" - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_webfsapi_endpoint", - side_effect=webfsapi_endpoint_error, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) + mock_afsapi.get_webfsapi_endpoint.side_effect = webfsapi_endpoint_error + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == result_error -async def test_ssdp_nondefault_pin(hass: HomeAssistant) -> None: +async def test_ssdp_nondefault_pin(hass: HomeAssistant, mock_afsapi: AsyncMock) -> None: """Test a device being discovered.""" - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_friendly_name", - side_effect=InvalidPinError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) + mock_afsapi.get_friendly_name.side_effect = InvalidPinError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -413,6 +395,7 @@ async def test_reauth_flow(hass: HomeAssistant, config_entry: MockConfigEntry) - ) async def test_reauth_flow_friendly_name_error( hass: HomeAssistant, + mock_afsapi: AsyncMock, exception: Exception, reason: str, config_entry: MockConfigEntry, @@ -425,20 +408,18 @@ async def test_reauth_flow_friendly_name_error( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_config" - with patch( - "homeassistant.components.frontier_silicon.config_flow.AFSAPI.get_friendly_name", - side_effect=exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PIN: "4321"}, - ) - await hass.async_block_till_done() + mock_afsapi.get_friendly_name.side_effect = exception + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] == {"base": reason} + mock_afsapi.get_friendly_name.side_effect = None result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "4242"}, diff --git a/tests/components/frontier_silicon/test_media_player.py b/tests/components/frontier_silicon/test_media_player.py index 615be17dfc95..9e7637a6865d 100644 --- a/tests/components/frontier_silicon/test_media_player.py +++ b/tests/components/frontier_silicon/test_media_player.py @@ -1,24 +1,54 @@ """Test the Frontier Silicon media player entity.""" -import logging -from unittest.mock import AsyncMock, patch +from datetime import timedelta +from unittest.mock import AsyncMock from afsapi import FSConnectionError, FSNotImplementedError, PlayCaps +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.frontier_silicon.media_player import AFSAPIMediaPlayer -from homeassistant.components.media_player import MediaPlayerEntityFeature -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.components.media_player import ( + DOMAIN as MEDIA_PLAYER_DOMAIN, + MediaPlayerEntityFeature, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_MEDIA_PREVIOUS_TRACK, + STATE_IDLE, + STATE_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity_component import async_update_entity -from .conftest import FakeAFSAPIDevice +from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -_LOGGER = logging.getLogger(__name__) +ENTITY_ID = "media_player.name_of_the_device" + +_FULL_PLAY_CAPS = ( + PlayCaps.PAUSE + | PlayCaps.STOP + | PlayCaps.SKIP_NEXT + | PlayCaps.SKIP_PREVIOUS + | PlayCaps.FAST_FORWARD + | PlayCaps.REWIND + | PlayCaps.SHUFFLE + | PlayCaps.REPEAT + | PlayCaps.SEEK + | PlayCaps.APPLY_FEEDBACK + | PlayCaps.SCROBBLING + | PlayCaps.ADD_PRESET + | PlayCaps.THUMBS_UP + | PlayCaps.THUMBS_DOWN + | PlayCaps.SKIP_FORWARD + | PlayCaps.SKIP_BACKWARD + | PlayCaps.REPEAT_ONE +) @pytest.mark.parametrize( @@ -33,16 +63,27 @@ _LOGGER = logging.getLogger(__name__) ], ) async def test_async_media_previous_track_maps_errors( - error: Exception, translation_key: str, message: str | None + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_afsapi: AsyncMock, + error: Exception, + translation_key: str, + message: str | None, ) -> None: """Test previous track maps API failures to Home Assistant errors.""" - fs_device = AsyncMock() - fs_device.rewind.side_effect = error - mock_config_entry = MockConfigEntry() - entity = AFSAPIMediaPlayer(mock_config_entry, fs_device) + mock_afsapi.get_power.return_value = True + mock_afsapi.get_play_caps.return_value = _FULL_PLAY_CAPS + mock_afsapi.rewind.side_effect = error + + await setup_integration(hass, config_entry) with pytest.raises(HomeAssistantError) as exc_info: - await entity.async_media_previous_track() + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) assert exc_info.value.translation_key == translation_key assert exc_info.value.translation_placeholders["command"] == "media_previous_track" @@ -52,33 +93,18 @@ async def test_async_media_previous_track_maps_errors( ) -async def test_async_media_caps() -> None: +async def test_async_media_caps( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_afsapi: AsyncMock, +) -> None: """Test AFSAPI play caps translation to MediaPlayerEntityFeatures.""" - fs_device = AsyncMock() - fs_device.get_power.return_value = False - fs_device.get_play_caps.return_value = ( - PlayCaps.PAUSE - | PlayCaps.STOP - | PlayCaps.SKIP_NEXT - | PlayCaps.SKIP_PREVIOUS - | PlayCaps.FAST_FORWARD - | PlayCaps.REWIND - | PlayCaps.SHUFFLE - | PlayCaps.REPEAT - | PlayCaps.SEEK - | PlayCaps.APPLY_FEEDBACK - | PlayCaps.SCROBBLING - | PlayCaps.ADD_PRESET - | PlayCaps.THUMBS_UP - | PlayCaps.THUMBS_DOWN - | PlayCaps.SKIP_FORWARD - | PlayCaps.SKIP_BACKWARD - | PlayCaps.REPEAT_ONE - ) - mock_config_entry = MockConfigEntry() - entity = AFSAPIMediaPlayer(mock_config_entry, fs_device) - await entity.async_update() - assert entity.supported_features == ( + mock_afsapi.get_play_caps.return_value = _FULL_PLAY_CAPS + + await setup_integration(hass, config_entry) + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_SUPPORTED_FEATURES] == ( AFSAPIMediaPlayer._BASE_SUPPORTED_FEATURES | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE @@ -97,32 +123,25 @@ async def test_media_player_on( config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - fake_afsapi_dev: FakeAFSAPIDevice, + mock_afsapi: AsyncMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test update of a device which is powered on.""" - # Connect device - with patch( - "homeassistant.components.frontier_silicon.AFSAPI", - FakeAFSAPIDevice, - ): - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, config_entry) - # Verify device exists devices = dr.async_entries_for_config_entry(device_registry, config_entry.entry_id) assert len(devices) == 1 device_entry = devices[0] - # Verify device has the expected number of entities - expected_num_entities = 1 entities = er.async_entries_for_device(entity_registry, device_entry.id) - assert len(entities) == expected_num_entities + assert len(entities) == 1 + + # Power on the device and advance time to trigger a poll + mock_afsapi.get_power.return_value = True + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - # Power on the fake device - fake_afsapi_dev.has_power = True - # get hass to do an update - await async_update_entity(hass, entities[0].entity_id) assert hass.states.get(entities[0].entity_id).state == STATE_IDLE @@ -131,43 +150,33 @@ async def test_async_update_disconnect( config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - fake_afsapi_dev: FakeAFSAPIDevice, + mock_afsapi: AsyncMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test that an update with a disconnect can change device availability.""" + await setup_integration(hass, config_entry) - # Connect device - with patch( - "homeassistant.components.frontier_silicon.AFSAPI", - FakeAFSAPIDevice, - ): - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - # Verify device exists devices = dr.async_entries_for_config_entry(device_registry, config_entry.entry_id) assert len(devices) == 1 device_entry = devices[0] - # Verify device has the expected number of entities - expected_num_entities = 1 entities = er.async_entries_for_device(entity_registry, device_entry.id) - assert len(entities) == expected_num_entities + assert len(entities) == 1 + entity_id = entities[0].entity_id - # Get hass to do an update - await async_update_entity(hass, entities[0].entity_id) - # Fake device starts in off state - assert hass.states.get(entities[0].entity_id).state == STATE_OFF + # Device starts in off state + assert hass.states.get(entity_id).state == STATE_OFF - # Make the fake device raise a connection error next time get_power is called - fake_afsapi_dev.fail_get_power = True - # get hass to do an update - await async_update_entity(hass, entities[0].entity_id) - # Check device availability, should now be offline - assert hass.states.get(entities[0].entity_id).state == STATE_UNAVAILABLE + # Make the device raise a connection error on the next poll + mock_afsapi.get_power.side_effect = FSConnectionError + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # Reset device error state - fake_afsapi_dev.fail_get_power = False - await async_update_entity(hass, entities[0].entity_id) - # Fake device should be back in off state - assert hass.states.get(entities[0].entity_id).state == STATE_OFF + mock_afsapi.get_power.side_effect = None + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF