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.
reauthentication-flow: done
test-coverage:
status: todo
comment: |
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.
status: done
comment: 99% test coverage
# Gold
devices: done
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
import pytest
from homeassistant.components import heos
from homeassistant.components.heos.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
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."""
controller.connect.side_effect = HeosError()
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["step_id"] == "user"
@ -60,7 +59,7 @@ async def test_create_entry_when_host_valid(
data = {CONF_HOST: "127.0.0.1"}
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["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)"}
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
@ -99,7 +98,7 @@ async def test_discovery_shows_create_form(
# Single discovered host shows form for user to finish setup.
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 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.
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] == {
"Office (127.0.0.1)": "127.0.0.1",

View File

@ -2,30 +2,22 @@
import asyncio
from typing import cast
from unittest.mock import Mock, patch
from pyheos import (
CommandFailedError,
Heos,
HeosError,
HeosOptions,
SignalHeosEvent,
SignalType,
const,
)
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.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@ -38,18 +30,14 @@ async def test_async_setup_entry_loads_platforms(
) -> None:
"""Test load connects to heos, retrieves players, and loads platforms."""
config_entry.add_to_hass(hass)
with patch.object(
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.get_players.call_count == 1
assert controller.get_favorites.call_count == 1
assert controller.get_input_sources.call_count == 1
controller.disconnect.assert_not_called()
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state == ConfigEntryState.LOADED
assert hass.states.get("media_player.test_player") is not None
assert controller.connect.call_count == 1
assert controller.get_players.call_count == 1
assert controller.get_favorites.call_count == 1
assert controller.get_input_sources.call_count == 1
controller.disconnect.assert_not_called()
async def test_async_setup_entry_with_options_loads_platforms(
@ -75,7 +63,7 @@ async def test_async_setup_entry_with_options_loads_platforms(
async def test_async_setup_entry_auth_failure_starts_reauth(
hass: HomeAssistant,
config_entry_options: MockConfigEntry,
controller: Mock,
controller: Heos,
) -> None:
"""Test load with auth failure starts reauth, loads platforms."""
config_entry_options.add_to_hass(hass)
@ -110,18 +98,12 @@ async def test_async_setup_entry_not_signed_in_loads_platforms(
"""Test setup does not retrieve favorites when not logged in."""
config_entry.add_to_hass(hass)
controller._signed_in_username = None
with patch.object(
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.get_players.call_count == 1
assert controller.get_favorites.call_count == 0
assert controller.get_input_sources.call_count == 1
controller.disconnect.assert_not_called()
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert controller.connect.call_count == 1
assert controller.get_players.call_count == 1
assert controller.get_favorites.call_count == 0
assert controller.get_input_sources.call_count == 1
controller.disconnect.assert_not_called()
assert (
"The HEOS System is not logged in: Enter credentials in the integration options to access favorites and streaming services"
in caplog.text
@ -134,8 +116,8 @@ async def test_async_setup_entry_connect_failure(
"""Connection failure raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
controller.connect.side_effect = HeosError()
with pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, config_entry)
assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state == ConfigEntryState.SETUP_RETRY
assert controller.connect.call_count == 1
assert controller.disconnect.call_count == 1
controller.connect.reset_mock()
@ -148,27 +130,21 @@ async def test_async_setup_entry_player_failure(
"""Failure to retrieve players/sources raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
controller.get_players.side_effect = HeosError()
with pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, config_entry)
assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert controller.connect.call_count == 1
assert controller.disconnect.call_count == 1
controller.connect.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."""
controller_manager = Mock(ControllerManager)
config_entry.runtime_data = HeosRuntimeData(controller_manager, None, None, {})
with patch.object(
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
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert controller.disconnect.call_count == 1
async def test_update_sources_retry(

View File

@ -18,15 +18,14 @@ from pyheos import (
const,
)
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.media_player import (
ATTR_GROUP_MEMBERS,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
@ -34,7 +33,6 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MEDIA_PLAYER_DOMAIN,
@ -43,13 +41,10 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOURCE,
SERVICE_UNJOIN,
MediaPlayerEntityFeature,
MediaType,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
@ -72,42 +67,20 @@ from tests.common import MockConfigEntry
@pytest.mark.usefixtures("controller")
async def test_state_attributes(
hass: HomeAssistant, config_entry: MockConfigEntry
hass: HomeAssistant, config_entry: MockConfigEntry, snapshot: SnapshotAssertion
) -> None:
"""Tests the state attributes."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
state = hass.states.get("media_player.test_player")
assert state.state == STATE_IDLE
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.25
assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED]
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == "1"
assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC
assert ATTR_MEDIA_DURATION not in state.attributes
assert ATTR_MEDIA_POSITION not in state.attributes
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
assert state == snapshot(
exclude=props(
"entity_picture_local",
"context",
"last_changed",
"last_reported",
"last_updated",
)
)