From 2e300aec5aba13d51da439cce009c70c4a1b2586 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Mon, 29 Jul 2019 06:38:17 -0700 Subject: [PATCH] Add PS4 tests for media player (#25415) * Add tests for media_player * remove ps4/media_player.py * Add unsubscribe method to unload * Add_to_hass instead of add_to_manager * Use hass.states for states * Fix assertions * fix tests * Add schedule update * Remove entity assertions --- .coveragerc | 1 - homeassistant/components/ps4/media_player.py | 2 + tests/components/ps4/test_media_player.py | 496 +++++++++++++++++++ 3 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 tests/components/ps4/test_media_player.py diff --git a/.coveragerc b/.coveragerc index 6261aef5120..7f3aa2762bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -476,7 +476,6 @@ omit = homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py homeassistant/components/proxy/camera.py - homeassistant/components/ps4/media_player.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index dfa8fe79e0b..aa1ed02a912 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -166,6 +166,7 @@ class PS4Device(MediaPlayerDevice): if self._use_saved(): _LOGGER.debug( "Using saved data for media: %s", title_id) + self.schedule_update() return self._media_title = name @@ -348,6 +349,7 @@ class PS4Device(MediaPlayerDevice): # Close TCP Transport. if self._ps4.connected: await self._ps4.close() + self.unsubscribe_to_protocol() self.hass.data[PS4_DATA].devices.remove(self) @property diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py new file mode 100644 index 00000000000..54855ebb471 --- /dev/null +++ b/tests/components/ps4/test_media_player.py @@ -0,0 +1,496 @@ +"""Tests for the PS4 media player platform.""" +from unittest.mock import MagicMock, patch + +from pyps4_homeassistant.credential import get_ddp_message + +from homeassistant.components import ps4 +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, MEDIA_TYPE_GAME) +from homeassistant.components.ps4.const import ( + ATTR_MEDIA_IMAGE_URL, CONFIG_ENTRY_VERSION as VERSION, + DEFAULT_REGION, DOMAIN, GAMES_FILE, PS4_DATA) +from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, ATTR_LOCKED, CONF_HOST, CONF_NAME, + CONF_REGION, CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING, + STATE_UNKNOWN) +from homeassistant.setup import async_setup_component +from tests.common import ( + MockConfigEntry, mock_device_registry, mock_registry, mock_coro) + + +MOCK_CREDS = '123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd' +MOCK_NAME = 'ha_ps4_name' +MOCK_REGION = DEFAULT_REGION +MOCK_GAMES_FILE = GAMES_FILE +MOCK_HOST = '192.168.0.2' +MOCK_HOST_NAME = 'Fake PS4' +MOCK_HOST_ID = 'A0000A0AA000' +MOCK_HOST_VERSION = '09879011' +MOCK_HOST_TYPE = 'PS4' +MOCK_STATUS_STANDBY = 'Server Standby' +MOCK_STATUS_ON = 'Ok' +MOCK_STANDBY_CODE = 620 +MOCK_ON_CODE = 200 +MOCK_TCP_PORT = 997 +MOCK_DDP_PORT = 987 +MOCK_DDP_VERSION = '00020020' +MOCK_RANDOM_PORT = '1234' + +MOCK_TITLE_ID = 'CUSA00000' +MOCK_TITLE_NAME = 'Random Game' +MOCK_TITLE_TYPE = MEDIA_TYPE_GAME +MOCK_TITLE_ART_URL = 'https://somecoverurl' + +MOCK_GAMES_DATA = { + ATTR_LOCKED: False, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_IMAGE_URL: MOCK_TITLE_ART_URL, + ATTR_MEDIA_TITLE: MOCK_TITLE_NAME} + +MOCK_GAMES_DATA_LOCKED = { + ATTR_LOCKED: True, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_IMAGE_URL: MOCK_TITLE_ART_URL, + ATTR_MEDIA_TITLE: MOCK_TITLE_NAME} + +MOCK_STATUS_PLAYING = { + 'host-type': MOCK_HOST_TYPE, + 'host-ip': MOCK_HOST, + 'host-request-port': MOCK_TCP_PORT, + 'host-id': MOCK_HOST_ID, + 'host-name': MOCK_HOST_NAME, + 'running-app-titleid': MOCK_TITLE_ID, + 'running-app-name': MOCK_TITLE_NAME, + 'status': MOCK_STATUS_ON, + 'status_code': MOCK_ON_CODE, + 'device-discovery-protocol-version': MOCK_DDP_VERSION, + 'system-version': MOCK_HOST_VERSION} + +MOCK_STATUS_IDLE = { + 'host-type': MOCK_HOST_TYPE, + 'host-ip': MOCK_HOST, + 'host-request-port': MOCK_TCP_PORT, + 'host-id': MOCK_HOST_ID, + 'host-name': MOCK_HOST_NAME, + 'status': MOCK_STATUS_ON, + 'status_code': MOCK_ON_CODE, + 'device-discovery-protocol-version': MOCK_DDP_VERSION, + 'system-version': MOCK_HOST_VERSION} + +MOCK_STATUS_OFF = { + 'host-type': MOCK_HOST_TYPE, + 'host-ip': MOCK_HOST, + 'host-request-port': MOCK_TCP_PORT, + 'host-id': MOCK_HOST_ID, + 'host-name': MOCK_HOST_NAME, + 'status': MOCK_STATUS_STANDBY, + 'status_code': MOCK_STANDBY_CODE, + 'device-discovery-protocol-version': MOCK_DDP_VERSION, + 'system-version': MOCK_HOST_VERSION} + +MOCK_DEVICE = { + CONF_HOST: MOCK_HOST, + CONF_NAME: MOCK_NAME, + CONF_REGION: MOCK_REGION +} + +MOCK_ENTRY_ID = 'SomeID' + +MOCK_DEVICE_MODEL = 'PlayStation 4' + +MOCK_DATA = { + CONF_TOKEN: MOCK_CREDS, + 'devices': [MOCK_DEVICE] +} + +MOCK_CONFIG = MockConfigEntry( + domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY_ID) + +MOCK_LOAD = 'homeassistant.components.ps4.media_player.load_games' +MOCK_SAVE = 'homeassistant.components.ps4.save_json' + + +async def setup_mock_component(hass, entry=None): + """Set up Mock Media Player.""" + if entry is None: + mock_entry = MockConfigEntry( + domain=ps4.DOMAIN, data=MOCK_DATA, version=VERSION, + entry_id=MOCK_ENTRY_ID) + else: + mock_entry = entry + + mock_entry.add_to_hass(hass) + + # Don't use an actual file. + with patch(MOCK_LOAD, return_value={}),\ + patch(MOCK_SAVE, side_effect=MagicMock()): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + await hass.async_block_till_done() + + mock_entities = hass.states.async_entity_ids() + + mock_entity_id = mock_entities[0] + + return mock_entity_id + + +async def mock_ddp_response(hass, mock_status_data, games=None): + """Mock raw UDP response from device.""" + mock_protocol = hass.data[PS4_DATA].protocol + + mock_code = mock_status_data.get('status_code') + mock_status = mock_status_data.get('status') + mock_status_header = '{} {}'.format(mock_code, mock_status) + mock_response = get_ddp_message( + mock_status_header, mock_status_data).encode() + + if games is None: + games = {} + + with patch(MOCK_LOAD, return_value=games),\ + patch(MOCK_SAVE, side_effect=MagicMock()): + mock_protocol.datagram_received( + mock_response, (MOCK_HOST, MOCK_RANDOM_PORT)) + await hass.async_block_till_done() + + +async def test_media_player_is_setup_correctly_with_entry(hass): + """Test entity is setup correctly with entry correctly.""" + mock_entity_id = await setup_mock_component(hass) + mock_state = hass.states.get(mock_entity_id).state + + # Assert status updated callback is added to protocol. + assert len(hass.data[PS4_DATA].protocol.callbacks) == 1 + + # Test that entity is added to hass. + assert hass.data[PS4_DATA].protocol is not None + assert mock_entity_id == 'media_player.{}'.format(MOCK_NAME) + assert mock_state == STATE_UNKNOWN + + +async def test_state_off_is_set(hass): + """Test that state is set to off.""" + mock_entity_id = await setup_mock_component(hass) + + await mock_ddp_response(hass, MOCK_STATUS_OFF) + + assert hass.states.get(mock_entity_id).state == STATE_OFF + + +async def test_state_playing_is_set(hass): + """Test that state is set to playing.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.async_get_ps_store_data') + + with patch(mock_func, return_value=mock_coro(None)),\ + patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + + assert hass.states.get(mock_entity_id).state == STATE_PLAYING + + +async def test_state_idle_is_set(hass): + """Test that state is set to idle.""" + mock_entity_id = await setup_mock_component(hass) + + await mock_ddp_response(hass, MOCK_STATUS_IDLE) + + assert hass.states.get(mock_entity_id).state == STATE_IDLE + + +async def test_state_none_is_set(hass): + """Test that state is set to None.""" + mock_entity_id = await setup_mock_component(hass) + + assert hass.states.get(mock_entity_id).state == STATE_UNKNOWN + + +async def test_media_attributes_are_fetched(hass): + """Test that media attributes are fetched.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.async_get_ps_store_data') + + # Mock result from fetching data. + mock_result = MagicMock() + mock_result.name = MOCK_TITLE_NAME + mock_result.cover_art = MOCK_TITLE_ART_URL + mock_result.game_type = 'game' + + with patch(mock_func, return_value=mock_coro(mock_result)) as mock_fetch,\ + patch(MOCK_SAVE, side_effect=MagicMock()): + + await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + + mock_state = hass.states.get(mock_entity_id) + mock_attrs = dict(mock_state.attributes) + + assert len(mock_fetch.mock_calls) == 1 + + assert mock_state.state == STATE_PLAYING + assert len(mock_attrs.get(ATTR_INPUT_SOURCE_LIST)) == 1 + assert mock_attrs.get(ATTR_INPUT_SOURCE_LIST)[0] == MOCK_TITLE_NAME + assert mock_attrs.get(ATTR_MEDIA_CONTENT_ID) == MOCK_TITLE_ID + assert mock_attrs.get(ATTR_MEDIA_TITLE) == MOCK_TITLE_NAME + assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE + + +async def test_media_attributes_are_loaded(hass): + """Test that media attributes are loaded.""" + mock_entity_id = await setup_mock_component(hass) + mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA_LOCKED} + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.async_get_ps_store_data') + + with patch(mock_func, return_value=mock_coro(None)) as mock_fetch: + await mock_ddp_response(hass, MOCK_STATUS_PLAYING, mock_data) + + mock_state = hass.states.get(mock_entity_id) + mock_attrs = dict(mock_state.attributes) + + # Ensure that data is not fetched. + assert not mock_fetch.mock_calls + + assert mock_state.state == STATE_PLAYING + + assert len(mock_attrs.get(ATTR_INPUT_SOURCE_LIST)) == 1 + assert mock_attrs.get(ATTR_INPUT_SOURCE_LIST)[0] == MOCK_TITLE_NAME + assert mock_attrs.get(ATTR_MEDIA_CONTENT_ID) == MOCK_TITLE_ID + assert mock_attrs.get(ATTR_MEDIA_TITLE) == MOCK_TITLE_NAME + assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE + + +async def test_device_info_is_set_from_status_correctly(hass): + """Test that device info is set correctly from status update.""" + mock_d_registry = mock_device_registry(hass) + with patch('pyps4_homeassistant.ps4.get_status', + return_value=MOCK_STATUS_OFF): + mock_entity_id = await setup_mock_component(hass) + + await hass.async_block_till_done() + + # Reformat mock status-sw_version for assertion. + mock_version = MOCK_STATUS_OFF['system-version'] + mock_version = mock_version[1:4] + mock_version = "{}.{}".format(mock_version[0], mock_version[1:]) + + mock_state = hass.states.get(mock_entity_id).state + + mock_d_entries = mock_d_registry.devices + mock_entry = mock_d_registry.async_get_device( + identifiers={(DOMAIN, MOCK_HOST_ID)}, connections={()}) + assert mock_state == STATE_OFF + + assert len(mock_d_entries) == 1 + assert mock_entry.name == MOCK_HOST_NAME + assert mock_entry.model == MOCK_DEVICE_MODEL + assert mock_entry.sw_version == mock_version + assert mock_entry.identifiers == { + (DOMAIN, MOCK_HOST_ID)} + + +async def test_device_info_is_assummed(hass): + """Test that device info is assumed if device is unavailable.""" + # Create a device registry entry with device info. + mock_d_registry = mock_device_registry(hass) + mock_d_registry.async_get_or_create( + config_entry_id=MOCK_ENTRY_ID, name=MOCK_HOST_NAME, + model=MOCK_DEVICE_MODEL, identifiers={(DOMAIN, MOCK_HOST_ID)}, + sw_version=MOCK_HOST_VERSION) + mock_d_entries = mock_d_registry.devices + assert len(mock_d_entries) == 1 + + # Create a entity_registry entry which is using identifiers from device. + mock_unique_id = ps4.format_unique_id(MOCK_CREDS, MOCK_HOST_ID) + mock_e_registry = mock_registry(hass) + mock_e_registry.async_get_or_create( + 'media_player', DOMAIN, mock_unique_id, config_entry_id=MOCK_ENTRY_ID) + mock_entity_id = mock_e_registry.async_get_entity_id( + 'media_player', DOMAIN, mock_unique_id) + + mock_entity_id = await setup_mock_component(hass) + mock_state = hass.states.get(mock_entity_id).state + + # Ensure that state is not set. + assert mock_state == STATE_UNKNOWN + + # Ensure that entity_id is the same as the existing. + mock_entities = hass.states.async_entity_ids() + assert len(mock_entities) == 1 + assert mock_entities[0] == mock_entity_id + + +async def test_device_info_assummed_works(hass): + """Reverse test that device info assumption works.""" + mock_d_registry = mock_device_registry(hass) + mock_entity_id = await setup_mock_component(hass) + mock_state = hass.states.get(mock_entity_id).state + mock_d_entries = mock_d_registry.devices + + # Ensure that state is not set. + assert mock_state == STATE_UNKNOWN + + # With no state/status and no existing entries, registry should be empty. + assert not mock_d_entries + + +async def test_turn_on(hass): + """Test that turn on service calls function.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.wakeup') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + await hass.services.async_call( + 'media_player', 'turn_on', + {ATTR_ENTITY_ID: mock_entity_id}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_turn_off(hass): + """Test that turn off service calls function.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.standby') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + await hass.services.async_call( + 'media_player', 'turn_off', + {ATTR_ENTITY_ID: mock_entity_id}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_media_pause(hass): + """Test that media pause service calls function.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.remote_control') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + await hass.services.async_call( + 'media_player', 'media_pause', + {ATTR_ENTITY_ID: mock_entity_id}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_media_stop(hass): + """Test that media stop service calls function.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.remote_control') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + await hass.services.async_call( + 'media_player', 'media_stop', + {ATTR_ENTITY_ID: mock_entity_id}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_select_source(hass): + """Test that select source service calls function with title.""" + mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch('pyps4_homeassistant.ps4.get_status', + return_value=MOCK_STATUS_IDLE),\ + patch(MOCK_LOAD, return_value=mock_data): + mock_entity_id = await setup_mock_component(hass) + + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.start_title') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + # Test with title name. + await hass.services.async_call( + 'media_player', 'select_source', + {ATTR_ENTITY_ID: mock_entity_id, + ATTR_INPUT_SOURCE: MOCK_TITLE_NAME}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_select_source_caps(hass): + """Test that select source service calls function with upper case title.""" + mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch('pyps4_homeassistant.ps4.get_status', + return_value=MOCK_STATUS_IDLE),\ + patch(MOCK_LOAD, return_value=mock_data): + mock_entity_id = await setup_mock_component(hass) + + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.start_title') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + # Test with title name in caps. + await hass.services.async_call( + 'media_player', 'select_source', + {ATTR_ENTITY_ID: mock_entity_id, + ATTR_INPUT_SOURCE: MOCK_TITLE_NAME.upper()}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_select_source_id(hass): + """Test that select source service calls function with Title ID.""" + mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch('pyps4_homeassistant.ps4.get_status', + return_value=MOCK_STATUS_IDLE),\ + patch(MOCK_LOAD, return_value=mock_data): + mock_entity_id = await setup_mock_component(hass) + + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.start_title') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + # Test with title ID. + await hass.services.async_call( + 'media_player', 'select_source', + {ATTR_ENTITY_ID: mock_entity_id, + ATTR_INPUT_SOURCE: MOCK_TITLE_ID}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_ps4_send_command(hass): + """Test that ps4 send command service calls function.""" + mock_entity_id = await setup_mock_component(hass) + mock_func = '{}{}'.format('homeassistant.components.ps4.media_player.', + 'pyps4.Ps4Async.remote_control') + + with patch(mock_func, return_value=MagicMock()) as mock_call: + await hass.services.async_call( + DOMAIN, 'send_command', + {ATTR_ENTITY_ID: mock_entity_id, + ATTR_COMMAND: 'ps'}) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_entry_is_unloaded(hass): + """Test that entry is unloaded.""" + mock_entry = MockConfigEntry( + domain=ps4.DOMAIN, data=MOCK_DATA, version=VERSION, + entry_id=MOCK_ENTRY_ID) + mock_entity_id = await setup_mock_component(hass, mock_entry) + mock_unload = await ps4.async_unload_entry(hass, mock_entry) + + assert mock_unload is True + assert not hass.data[PS4_DATA].devices + + # Test that callback listener for entity is removed from protocol. + assert not hass.data[PS4_DATA].protocol.callbacks + + assert hass.states.get(mock_entity_id) is None