mirror of https://github.com/home-assistant/core
551 lines
18 KiB
Python
551 lines
18 KiB
Python
"""Tests for the Spotify media player platform."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from spotifyaio import (
|
|
PlaybackState,
|
|
ProductType,
|
|
RepeatMode as SpotifyRepeatMode,
|
|
SpotifyConnectionError,
|
|
)
|
|
from syrupy import SnapshotAssertion
|
|
|
|
from homeassistant.components.media_player import (
|
|
ATTR_INPUT_SOURCE,
|
|
ATTR_INPUT_SOURCE_LIST,
|
|
ATTR_MEDIA_CONTENT_ID,
|
|
ATTR_MEDIA_CONTENT_TYPE,
|
|
ATTR_MEDIA_ENQUEUE,
|
|
ATTR_MEDIA_REPEAT,
|
|
ATTR_MEDIA_SEEK_POSITION,
|
|
ATTR_MEDIA_SHUFFLE,
|
|
ATTR_MEDIA_VOLUME_LEVEL,
|
|
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_PLAY_MEDIA,
|
|
SERVICE_SELECT_SOURCE,
|
|
MediaPlayerEnqueue,
|
|
MediaPlayerEntityFeature,
|
|
MediaPlayerState,
|
|
MediaType,
|
|
RepeatMode,
|
|
)
|
|
from homeassistant.components.spotify import DOMAIN
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
ATTR_ENTITY_PICTURE,
|
|
SERVICE_MEDIA_NEXT_TRACK,
|
|
SERVICE_MEDIA_PAUSE,
|
|
SERVICE_MEDIA_PLAY,
|
|
SERVICE_MEDIA_PREVIOUS_TRACK,
|
|
SERVICE_MEDIA_SEEK,
|
|
SERVICE_REPEAT_SET,
|
|
SERVICE_SHUFFLE_SET,
|
|
SERVICE_VOLUME_SET,
|
|
STATE_UNAVAILABLE,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from . import setup_integration
|
|
|
|
from tests.common import (
|
|
MockConfigEntry,
|
|
async_fire_time_changed,
|
|
load_fixture,
|
|
snapshot_platform,
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_entities(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
entity_registry: er.EntityRegistry,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test the Spotify entities."""
|
|
freezer.move_to("2023-10-21")
|
|
with (
|
|
patch("secrets.token_hex", return_value="mock-token"),
|
|
patch("homeassistant.components.spotify.PLATFORMS", [Platform.MEDIA_PLAYER]),
|
|
):
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
await snapshot_platform(
|
|
hass, entity_registry, snapshot, mock_config_entry.entry_id
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_podcast(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
entity_registry: er.EntityRegistry,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test the Spotify entities while listening a podcast."""
|
|
freezer.move_to("2023-10-21")
|
|
mock_spotify.return_value.get_playback.return_value = PlaybackState.from_json(
|
|
load_fixture("playback_episode.json", DOMAIN)
|
|
)
|
|
with (
|
|
patch("secrets.token_hex", return_value="mock-token"),
|
|
patch("homeassistant.components.spotify.PLATFORMS", [Platform.MEDIA_PLAYER]),
|
|
):
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
await snapshot_platform(
|
|
hass, entity_registry, snapshot, mock_config_entry.entry_id
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_free_account(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify entities with a free account."""
|
|
mock_spotify.return_value.get_current_user.return_value.product = ProductType.FREE
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert state.attributes["supported_features"] == 0
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_restricted_device(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify entities with a restricted device."""
|
|
mock_spotify.return_value.get_playback.return_value.device.is_restricted = True
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert (
|
|
state.attributes["supported_features"] == MediaPlayerEntityFeature.SELECT_SOURCE
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_spotify_dj_list(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify entities with a Spotify DJ playlist."""
|
|
mock_spotify.return_value.get_playback.return_value.context.uri = (
|
|
"spotify:playlist:37i9dQZF1EYkqdzj48dyYq"
|
|
)
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert state.attributes["media_playlist"] == "DJ"
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_fetching_playlist_does_not_fail(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test failing fetching playlist does not fail update."""
|
|
mock_spotify.return_value.get_playlist.side_effect = SpotifyConnectionError
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert "media_playlist" not in state.attributes
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_idle(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify entities in idle state."""
|
|
mock_spotify.return_value.get_playback.return_value = {}
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert state.state == MediaPlayerState.IDLE
|
|
assert (
|
|
state.attributes["supported_features"] == MediaPlayerEntityFeature.SELECT_SOURCE
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
@pytest.mark.parametrize(
|
|
("service", "method"),
|
|
[
|
|
(SERVICE_MEDIA_PLAY, "start_playback"),
|
|
(SERVICE_MEDIA_PAUSE, "pause_playback"),
|
|
(SERVICE_MEDIA_PREVIOUS_TRACK, "previous_track"),
|
|
(SERVICE_MEDIA_NEXT_TRACK, "next_track"),
|
|
],
|
|
)
|
|
async def test_simple_actions(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
method: str,
|
|
) -> None:
|
|
"""Test the Spotify media player."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
service,
|
|
{ATTR_ENTITY_ID: "media_player.spotify_spotify_1"},
|
|
blocking=True,
|
|
)
|
|
getattr(mock_spotify.return_value, method).assert_called_once_with()
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_repeat_mode(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player repeat mode."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
for mode, spotify_mode in (
|
|
(RepeatMode.ALL, SpotifyRepeatMode.CONTEXT),
|
|
(RepeatMode.ONE, SpotifyRepeatMode.TRACK),
|
|
(RepeatMode.OFF, SpotifyRepeatMode.OFF),
|
|
):
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_REPEAT_SET,
|
|
{ATTR_ENTITY_ID: "media_player.spotify_spotify_1", ATTR_MEDIA_REPEAT: mode},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.set_repeat.assert_called_once_with(spotify_mode)
|
|
mock_spotify.return_value.set_repeat.reset_mock()
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_shuffle(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player shuffle."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
for shuffle in (True, False):
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_SHUFFLE_SET,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_SHUFFLE: shuffle,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.set_shuffle.assert_called_once_with(state=shuffle)
|
|
mock_spotify.return_value.set_shuffle.reset_mock()
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_volume_level(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player volume level."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_VOLUME_SET,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.set_volume.assert_called_with(50)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_seek(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player seeking."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_MEDIA_SEEK,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_SEEK_POSITION: 100,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.seek_track.assert_called_with(100000)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
@pytest.mark.parametrize(
|
|
("media_type", "media_id"),
|
|
[
|
|
("spotify://track", "spotify:track:3oRoMXsP2NRzm51lldj1RO"),
|
|
("spotify://episode", "spotify:episode:3oRoMXsP2NRzm51lldj1RO"),
|
|
(MediaType.MUSIC, "spotify:track:3oRoMXsP2NRzm51lldj1RO"),
|
|
],
|
|
)
|
|
async def test_play_media_in_queue(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
media_type: str,
|
|
media_id: str,
|
|
) -> None:
|
|
"""Test the Spotify media player play media."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_PLAY_MEDIA,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_CONTENT_TYPE: media_type,
|
|
ATTR_MEDIA_CONTENT_ID: media_id,
|
|
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.add_to_queue.assert_called_with(media_id, None)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
@pytest.mark.parametrize(
|
|
("media_type", "media_id", "called_with"),
|
|
[
|
|
(
|
|
"spotify://artist",
|
|
"spotify:artist:74Yus6IHfa3tWZzXXAYtS2",
|
|
{"context_uri": "spotify:artist:74Yus6IHfa3tWZzXXAYtS2"},
|
|
),
|
|
(
|
|
"spotify://playlist",
|
|
"spotify:playlist:74Yus6IHfa3tWZzXXAYtS2",
|
|
{"context_uri": "spotify:playlist:74Yus6IHfa3tWZzXXAYtS2"},
|
|
),
|
|
(
|
|
"spotify://album",
|
|
"spotify:album:74Yus6IHfa3tWZzXXAYtS2",
|
|
{"context_uri": "spotify:album:74Yus6IHfa3tWZzXXAYtS2"},
|
|
),
|
|
(
|
|
"spotify://show",
|
|
"spotify:show:74Yus6IHfa3tWZzXXAYtS2",
|
|
{"context_uri": "spotify:show:74Yus6IHfa3tWZzXXAYtS2"},
|
|
),
|
|
(
|
|
MediaType.MUSIC,
|
|
"spotify:track:3oRoMXsP2NRzm51lldj1RO",
|
|
{"uris": ["spotify:track:3oRoMXsP2NRzm51lldj1RO"]},
|
|
),
|
|
(
|
|
"spotify://track",
|
|
"spotify:track:3oRoMXsP2NRzm51lldj1RO",
|
|
{"uris": ["spotify:track:3oRoMXsP2NRzm51lldj1RO"]},
|
|
),
|
|
(
|
|
"spotify://episode",
|
|
"spotify:episode:3oRoMXsP2NRzm51lldj1RO",
|
|
{"uris": ["spotify:episode:3oRoMXsP2NRzm51lldj1RO"]},
|
|
),
|
|
],
|
|
)
|
|
async def test_play_media(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
media_type: str,
|
|
media_id: str,
|
|
called_with: dict,
|
|
) -> None:
|
|
"""Test the Spotify media player play media."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_PLAY_MEDIA,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_CONTENT_TYPE: media_type,
|
|
ATTR_MEDIA_CONTENT_ID: media_id,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.start_playback.assert_called_with(**called_with)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_add_unsupported_media_to_queue(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player add unsupported media to queue."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
with pytest.raises(
|
|
ValueError, match="Media type playlist is not supported when enqueue is ADD"
|
|
):
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_PLAY_MEDIA,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_CONTENT_TYPE: "spotify://playlist",
|
|
ATTR_MEDIA_CONTENT_ID: "spotify:playlist:74Yus6IHfa3tWZzXXAYtS2",
|
|
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD,
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_play_unsupported_media(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player play media."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_PLAY_MEDIA,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_MEDIA_CONTENT_TYPE: MediaType.COMPOSER,
|
|
ATTR_MEDIA_CONTENT_ID: "spotify:track:3oRoMXsP2NRzm51lldj1RO",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_spotify.return_value.start_playback.call_count == 0
|
|
assert mock_spotify.return_value.add_to_queue.call_count == 0
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_select_source(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player source select."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.services.async_call(
|
|
MEDIA_PLAYER_DOMAIN,
|
|
SERVICE_SELECT_SOURCE,
|
|
{
|
|
ATTR_ENTITY_ID: "media_player.spotify_spotify_1",
|
|
ATTR_INPUT_SOURCE: "DESKTOP-BKC5SIK",
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_spotify.return_value.transfer_playback.assert_called_with(
|
|
"21dac6b0e0a1f181870fdc9749b2656466557666"
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_source_devices(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test the Spotify media player available source devices."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
|
|
assert state.attributes[ATTR_INPUT_SOURCE_LIST] == ["DESKTOP-BKC5SIK"]
|
|
|
|
mock_spotify.return_value.get_devices.side_effect = SpotifyConnectionError
|
|
freezer.tick(timedelta(minutes=5))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert state.state != STATE_UNAVAILABLE
|
|
assert state.attributes[ATTR_INPUT_SOURCE_LIST] == ["DESKTOP-BKC5SIK"]
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_paused_playback(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player with paused playback."""
|
|
mock_spotify.return_value.get_playback.return_value.is_playing = False
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert state.state == MediaPlayerState.PAUSED
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_fallback_show_image(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player with a fallback image."""
|
|
playback = PlaybackState.from_json(load_fixture("playback_episode.json", DOMAIN))
|
|
playback.item.images = []
|
|
mock_spotify.return_value.get_playback.return_value = playback
|
|
with patch("secrets.token_hex", return_value="mock-token"):
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert (
|
|
state.attributes[ATTR_ENTITY_PICTURE]
|
|
== "/api/media_player_proxy/media_player.spotify_spotify_1?token=mock-token&cache=16ff384dbae94fea"
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_no_episode_images(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player with no episode images."""
|
|
playback = PlaybackState.from_json(load_fixture("playback_episode.json", DOMAIN))
|
|
playback.item.images = []
|
|
playback.item.show.images = []
|
|
mock_spotify.return_value.get_playback.return_value = playback
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert ATTR_ENTITY_PICTURE not in state.attributes
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_no_album_images(
|
|
hass: HomeAssistant,
|
|
mock_spotify: MagicMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the Spotify media player with no album images."""
|
|
mock_spotify.return_value.get_playback.return_value.item.album.images = []
|
|
await setup_integration(hass, mock_config_entry)
|
|
state = hass.states.get("media_player.spotify_spotify_1")
|
|
assert state
|
|
assert ATTR_ENTITY_PICTURE not in state.attributes
|