Update HEOS tests to not patch internals (#136136)

This commit is contained in:
Andrew Sayre 2025-01-21 01:26:34 -06:00 committed by GitHub
parent f6b444b24b
commit 79a43b8a50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 103 deletions

View File

@ -38,15 +38,8 @@ rules:
comment: Needs to be set to 0. The underlying library handles parallel updates. comment: Needs to be set to 0. The underlying library handles parallel updates.
reauthentication-flow: done reauthentication-flow: done
test-coverage: test-coverage:
status: todo status: done
comment: | comment: 99% test coverage
1. Integration has >95% coverage, however tests need to be updated to not patch internals.
2. test_async_setup_entry_connect_failure and test_async_setup_entry_player_failure -> Instead of
calling async_setup_entry directly, rather use hass.config_entries.async_setup and then assert
the config_entry.state is what we expect.
3. test_unload_entry -> We should use hass.config_entries.async_unload and assert the entry state
4. Recommend using snapshot in test_state_attributes.
5. Find a way to avoid using internal dispatcher in test_updates_from_connection_event.
# Gold # Gold
devices: done devices: done
diagnostics: todo diagnostics: todo

View File

@ -0,0 +1,34 @@
# serializer version: 1
# name: test_state_attributes
StateSnapshot({
'attributes': ReadOnlyDict({
'entity_picture': 'http://',
'friendly_name': 'Test Player',
'group_members': list([
'media_player.test_player',
'media_player.test_player_2',
]),
'is_volume_muted': False,
'media_album_id': 1,
'media_album_name': 'Album',
'media_artist': 'Artist',
'media_content_id': '1',
'media_content_type': <MediaType.MUSIC: 'music'>,
'media_queue_id': 1,
'media_source_id': 1,
'media_station': 'Station Name',
'media_title': 'Song',
'media_type': 'Station',
'shuffle': False,
'source_list': list([
"Today's Hits Radio",
'Classical MPR (Classical Music)',
'HEOS Drive - Line In 1',
]),
'supported_features': <MediaPlayerEntityFeature: 2817597>,
'volume_level': 0.25,
}),
'entity_id': 'media_player.test_player',
'state': 'idle',
})
# ---

View File

@ -3,7 +3,6 @@
from pyheos import CommandAuthenticationError, CommandFailedError, Heos, HeosError from pyheos import CommandAuthenticationError, CommandFailedError, Heos, HeosError
import pytest import pytest
from homeassistant.components import heos
from homeassistant.components.heos.const import DOMAIN from homeassistant.components.heos.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
@ -44,7 +43,7 @@ async def test_cannot_connect_shows_error_form(
"""Test form is shown with error when cannot connect.""" """Test form is shown with error when cannot connect."""
controller.connect.side_effect = HeosError() controller.connect.side_effect = HeosError()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"}
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
@ -60,7 +59,7 @@ async def test_create_entry_when_host_valid(
data = {CONF_HOST: "127.0.0.1"} data = {CONF_HOST: "127.0.0.1"}
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
heos.DOMAIN, context={"source": SOURCE_USER}, data=data DOMAIN, context={"source": SOURCE_USER}, data=data
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["result"].unique_id == DOMAIN assert result["result"].unique_id == DOMAIN
@ -78,7 +77,7 @@ async def test_create_entry_when_friendly_name_valid(
data = {CONF_HOST: "Office (127.0.0.1)"} data = {CONF_HOST: "Office (127.0.0.1)"}
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
heos.DOMAIN, context={"source": SOURCE_USER}, data=data DOMAIN, context={"source": SOURCE_USER}, data=data
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
@ -99,7 +98,7 @@ async def test_discovery_shows_create_form(
# Single discovered host shows form for user to finish setup. # Single discovered host shows form for user to finish setup.
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data
) )
assert hass.data[DOMAIN] == {"Office (127.0.0.1)": "127.0.0.1"} assert hass.data[DOMAIN] == {"Office (127.0.0.1)": "127.0.0.1"}
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -107,7 +106,7 @@ async def test_discovery_shows_create_form(
# Subsequent discovered hosts append to discovered hosts and abort. # Subsequent discovered hosts append to discovered hosts and abort.
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data_bedroom DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data_bedroom
) )
assert hass.data[DOMAIN] == { assert hass.data[DOMAIN] == {
"Office (127.0.0.1)": "127.0.0.1", "Office (127.0.0.1)": "127.0.0.1",

View File

@ -2,30 +2,22 @@
import asyncio import asyncio
from typing import cast from typing import cast
from unittest.mock import Mock, patch
from pyheos import ( from pyheos import (
CommandFailedError, CommandFailedError,
Heos, Heos,
HeosError, HeosError,
HeosOptions,
SignalHeosEvent, SignalHeosEvent,
SignalType, SignalType,
const, const,
) )
import pytest import pytest
from homeassistant.components.heos import (
ControllerManager,
HeosOptions,
HeosRuntimeData,
async_setup_entry,
async_unload_entry,
)
from homeassistant.components.heos.const import DOMAIN from homeassistant.components.heos.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -38,13 +30,9 @@ async def test_async_setup_entry_loads_platforms(
) -> None: ) -> None:
"""Test load connects to heos, retrieves players, and loads platforms.""" """Test load connects to heos, retrieves players, and loads platforms."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch.object( assert await hass.config_entries.async_setup(config_entry.entry_id)
hass.config_entries, "async_forward_entry_setups" assert config_entry.state == ConfigEntryState.LOADED
) as forward_mock: assert hass.states.get("media_player.test_player") is not None
assert await async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
assert forward_mock.call_count == 1
assert controller.connect.call_count == 1 assert controller.connect.call_count == 1
assert controller.get_players.call_count == 1 assert controller.get_players.call_count == 1
assert controller.get_favorites.call_count == 1 assert controller.get_favorites.call_count == 1
@ -75,7 +63,7 @@ async def test_async_setup_entry_with_options_loads_platforms(
async def test_async_setup_entry_auth_failure_starts_reauth( async def test_async_setup_entry_auth_failure_starts_reauth(
hass: HomeAssistant, hass: HomeAssistant,
config_entry_options: MockConfigEntry, config_entry_options: MockConfigEntry,
controller: Mock, controller: Heos,
) -> None: ) -> None:
"""Test load with auth failure starts reauth, loads platforms.""" """Test load with auth failure starts reauth, loads platforms."""
config_entry_options.add_to_hass(hass) config_entry_options.add_to_hass(hass)
@ -110,13 +98,7 @@ async def test_async_setup_entry_not_signed_in_loads_platforms(
"""Test setup does not retrieve favorites when not logged in.""" """Test setup does not retrieve favorites when not logged in."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
controller._signed_in_username = None controller._signed_in_username = None
with patch.object( assert await hass.config_entries.async_setup(config_entry.entry_id)
hass.config_entries, "async_forward_entry_setups"
) as forward_mock:
assert await async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
assert forward_mock.call_count == 1
assert controller.connect.call_count == 1 assert controller.connect.call_count == 1
assert controller.get_players.call_count == 1 assert controller.get_players.call_count == 1
assert controller.get_favorites.call_count == 0 assert controller.get_favorites.call_count == 0
@ -134,8 +116,8 @@ async def test_async_setup_entry_connect_failure(
"""Connection failure raises ConfigEntryNotReady.""" """Connection failure raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
controller.connect.side_effect = HeosError() controller.connect.side_effect = HeosError()
with pytest.raises(ConfigEntryNotReady): assert not await hass.config_entries.async_setup(config_entry.entry_id)
await async_setup_entry(hass, config_entry) assert config_entry.state == ConfigEntryState.SETUP_RETRY
assert controller.connect.call_count == 1 assert controller.connect.call_count == 1
assert controller.disconnect.call_count == 1 assert controller.disconnect.call_count == 1
controller.connect.reset_mock() controller.connect.reset_mock()
@ -148,27 +130,21 @@ async def test_async_setup_entry_player_failure(
"""Failure to retrieve players/sources raises ConfigEntryNotReady.""" """Failure to retrieve players/sources raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
controller.get_players.side_effect = HeosError() controller.get_players.side_effect = HeosError()
with pytest.raises(ConfigEntryNotReady): assert not await hass.config_entries.async_setup(config_entry.entry_id)
await async_setup_entry(hass, config_entry)
assert controller.connect.call_count == 1 assert controller.connect.call_count == 1
assert controller.disconnect.call_count == 1 assert controller.disconnect.call_count == 1
controller.connect.reset_mock() controller.connect.reset_mock()
controller.disconnect.reset_mock() controller.disconnect.reset_mock()
async def test_unload_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: async def test_unload_entry(
hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos
) -> None:
"""Test entries are unloaded correctly.""" """Test entries are unloaded correctly."""
controller_manager = Mock(ControllerManager) config_entry.add_to_hass(hass)
config_entry.runtime_data = HeosRuntimeData(controller_manager, None, None, {}) assert await hass.config_entries.async_setup(config_entry.entry_id)
assert await hass.config_entries.async_unload(config_entry.entry_id)
with patch.object( assert controller.disconnect.call_count == 1
hass.config_entries, "async_forward_entry_unload", return_value=True
) as unload:
assert await async_unload_entry(hass, config_entry)
await hass.async_block_till_done()
assert controller_manager.disconnect.call_count == 1
assert unload.call_count == 1
assert DOMAIN not in hass.data
async def test_update_sources_retry( async def test_update_sources_retry(

View File

@ -18,15 +18,14 @@ from pyheos import (
const, const,
) )
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.heos import media_player
from homeassistant.components.heos.const import DOMAIN, SIGNAL_HEOS_UPDATED from homeassistant.components.heos.const import DOMAIN, SIGNAL_HEOS_UPDATED
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
ATTR_GROUP_MEMBERS, ATTR_GROUP_MEMBERS,
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST, ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION, ATTR_MEDIA_DURATION,
@ -34,7 +33,6 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MEDIA_PLAYER_DOMAIN, DOMAIN as MEDIA_PLAYER_DOMAIN,
@ -43,13 +41,10 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
SERVICE_UNJOIN, SERVICE_UNJOIN,
MediaPlayerEntityFeature,
MediaType, MediaType,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY,
@ -72,42 +67,20 @@ from tests.common import MockConfigEntry
@pytest.mark.usefixtures("controller") @pytest.mark.usefixtures("controller")
async def test_state_attributes( async def test_state_attributes(
hass: HomeAssistant, config_entry: MockConfigEntry hass: HomeAssistant, config_entry: MockConfigEntry, snapshot: SnapshotAssertion
) -> None: ) -> None:
"""Tests the state attributes.""" """Tests the state attributes."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
state = hass.states.get("media_player.test_player") state = hass.states.get("media_player.test_player")
assert state.state == STATE_IDLE assert state == snapshot(
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.25 exclude=props(
assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] "entity_picture_local",
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == "1" "context",
assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC "last_changed",
assert ATTR_MEDIA_DURATION not in state.attributes "last_reported",
assert ATTR_MEDIA_POSITION not in state.attributes "last_updated",
assert state.attributes[ATTR_MEDIA_TITLE] == "Song"
assert state.attributes[ATTR_MEDIA_ARTIST] == "Artist"
assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Album"
assert not state.attributes[ATTR_MEDIA_SHUFFLE]
assert state.attributes["media_album_id"] == 1
assert state.attributes["media_queue_id"] == 1
assert state.attributes["media_source_id"] == 1
assert state.attributes["media_station"] == "Station Name"
assert state.attributes["media_type"] == "Station"
assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Player"
assert (
state.attributes[ATTR_SUPPORTED_FEATURES]
== MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| media_player.BASE_SUPPORTED_FEATURES
) )
assert ATTR_INPUT_SOURCE not in state.attributes
assert (
state.attributes[ATTR_INPUT_SOURCE_LIST]
== config_entry.runtime_data.source_manager.source_list
) )