From dd2b092b7076d1d0dbc31a40bf7e31c52f554750 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 20 Dec 2024 18:53:58 -0600 Subject: [PATCH] more tweaks --- .../components/roku/snapshots/test_init.ambr | 41 +++ .../roku/snapshots/test_media_player.ambr | 8 +- tests/components/roku/test_config_flow.py | 294 +++++++++++------- tests/components/roku/test_init.py | 17 + tests/components/roku/test_media_player.py | 14 +- 5 files changed, 248 insertions(+), 126 deletions(-) create mode 100644 tests/components/roku/snapshots/test_init.ambr diff --git a/tests/components/roku/snapshots/test_init.ambr b/tests/components/roku/snapshots/test_init.ambr new file mode 100644 index 00000000000..25c6360c5e1 --- /dev/null +++ b/tests/components/roku/snapshots/test_init.ambr @@ -0,0 +1,41 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'b0:a7:37:96:4d:fa', + ), + tuple( + 'mac', + 'b0:a7:37:96:4d:fb', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '4200X', + 'id': , + 'identifiers': set({ + tuple( + 'roku', + '1GU48T017973', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Roku', + 'model': 'Roku 3', + 'model_id': None, + 'name': 'My Roku 3', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '7.5.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/roku/snapshots/test_media_player.ambr b/tests/components/roku/snapshots/test_media_player.ambr index 6e8932f3226..c8f8d0cd0e5 100644 --- a/tests/components/roku/snapshots/test_media_player.ambr +++ b/tests/components/roku/snapshots/test_media_player.ambr @@ -50,7 +50,7 @@ 'app_id': '12', 'app_name': 'Netflix', 'device_class': 'receiver', - 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=302b9654ec1a8c4fa9872fba3795edda5c07aa9c4d9a5231bf967376c91ceb1e&cache=2cab7007c60e13ef', + 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=fa35b32224ca3c83235d621f684a24389bf26b4acd4f2daff53ae76433427864&cache=2cab7007c60e13ef', 'friendly_name': 'My Roku 3', 'media_content_type': , 'source': 'Netflix', @@ -199,7 +199,7 @@ 'app_id': '74519', 'app_name': "Pluto TV - It's Free TV", 'device_class': 'receiver', - 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=7c360550a5a5274ff9e9e79ebc73ae1044c37851e47a0ff58cd36cab59eda988&cache=be54f0f123f7d91f', + 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=d91134eba0c155326d21ca70ef3c57b0e47cfb8da439b64a03016b40668ee3f7&cache=be54f0f123f7d91f', 'friendly_name': 'My Roku 3', 'media_content_type': , 'media_duration': 6496, @@ -278,7 +278,7 @@ 'app_id': '74519', 'app_name': "Pluto TV - It's Free TV", 'device_class': 'receiver', - 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=0b7d72bb6e62507f8efe49db9bbca12f4b803e52969cdd797cd8121b74f2de27&cache=be54f0f123f7d91f', + 'entity_picture': '/api/media_player_proxy/media_player.my_roku_3?token=3961658cb01e3e6f9d3194a0487e1043616ede6c9ef18c7033650e09160226ec&cache=be54f0f123f7d91f', 'friendly_name': 'My Roku 3', 'media_content_type': , 'media_duration': 6496, @@ -506,7 +506,7 @@ 'app_id': 'tvinput.dtv', 'app_name': 'Antenna TV', 'device_class': 'tv', - 'entity_picture': '/api/media_player_proxy/media_player.58_onn_roku_tv?token=0304dee053f24675c326f5cda374eb4cebf7561bc8c3da18561632f5fda38ae6&cache=545b6ca11153d83a', + 'entity_picture': '/api/media_player_proxy/media_player.58_onn_roku_tv?token=cc580699c8223d0f71d05c4de40776ae72df5981efeb24b5ee5725b19dd8b9b7&cache=545b6ca11153d83a', 'friendly_name': '58" Onn Roku TV', 'media_channel': 'getTV (14.3)', 'media_content_type': , diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 09f7994a33b..d426eaa1142 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,10 +1,13 @@ """Test the Roku config flow.""" -import dataclasses from unittest.mock import AsyncMock, MagicMock import pytest -from rokuecp import Device as RokuDevice, RokuConnectionError +from rokuecp import ( + Device as RokuDevice, + RokuConnectionError, + RokuConnectionTimeoutError, +) from homeassistant.components.roku.const import CONF_PLAY_MEDIA_APP_ID, DOMAIN from homeassistant.config_entries import ( @@ -31,39 +34,6 @@ from tests.common import MockConfigEntry RECONFIGURE_HOST = "192.168.1.190" -async def test_duplicate_error( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_roku_config_flow: MagicMock, -) -> None: - """Test that errors are shown when duplicates are added.""" - mock_config_entry.add_to_hass(hass) - - user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - async def test_form( hass: HomeAssistant, mock_roku_config_flow: MagicMock, @@ -73,6 +43,9 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) + + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -80,6 +53,7 @@ async def test_form( result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=user_input ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY @@ -92,77 +66,100 @@ async def test_form( assert result["result"].unique_id == "1GU48T017973" -async def test_form_cannot_connect( - hass: HomeAssistant, mock_roku_config_flow: MagicMock +@pytest.mark.parametrize( + ("error", "reason"), + [ + (RokuConnectionError, "cannot_connect"), + (RokuConnectionTimeoutError, "cannot_connect"), + ], +) +async def test_form_error( + hass: HomeAssistant, + mock_roku_config_flow: MagicMock, + error: Exception, + reason: str, ) -> None: - """Test we handle cannot connect roku error.""" - mock_roku_config_flow.update.side_effect = RokuConnectionError + """Test we handle usrr flow on error.""" + mock_roku_config_flow.update.side_effect = error result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"] == {"base": reason} + + mock_roku_config_flow.update.side_effect = None + + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_form_unknown_error( hass: HomeAssistant, mock_roku_config_flow: MagicMock ) -> None: - """Test we handle unknown error.""" + """Test we handle user flow on unknown error.""" mock_roku_config_flow.update.side_effect = Exception result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) + await hass.async_block_till_done() + user_input = {CONF_HOST: HOST} result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unknown" - - -async def test_homekit_cannot_connect( - hass: HomeAssistant, mock_roku_config_flow: MagicMock -) -> None: - """Test we abort homekit flow on connection error.""" - mock_roku_config_flow.update.side_effect = RokuConnectionError - - discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_HOMEKIT}, - data=discovery_info, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - -async def test_homekit_unknown_error( - hass: HomeAssistant, mock_roku_config_flow: MagicMock -) -> None: - """Test we abort homekit flow on unknown error.""" - mock_roku_config_flow.update.side_effect = Exception - - discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_HOMEKIT}, - data=discovery_info, - ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" +async def test_form_duplicate_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roku_config_flow: MagicMock, +) -> None: + """Test that we handle user flow on duplicates.""" + mock_config_entry.add_to_hass(hass) + + user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize("mock_device", ["rokutv-7820x"], indirect=True) async def test_homekit_discovery( hass: HomeAssistant, @@ -170,11 +167,13 @@ async def test_homekit_discovery( mock_setup_entry: None, ) -> None: """Test the homekit discovery flow.""" - discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -182,70 +181,80 @@ async def test_homekit_discovery( result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={} ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV + assert "result" in result + assert result["result"].unique_id == "YN00H5555555" + assert "data" in result assert result["data"][CONF_HOST] == HOMEKIT_HOST assert result["data"][CONF_NAME] == NAME_ROKUTV - # test abort on existing host - discovery_info = dataclasses.replace(MOCK_HOMEKIT_DISCOVERY_INFO) + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (RokuConnectionError, "cannot_connect"), + (RokuConnectionTimeoutError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_homekit_error( + hass: HomeAssistant, + mock_roku_config_flow: MagicMock, + error: Exception, + reason: str, +) -> None: + """Test we abort Homekit flow on error.""" + mock_roku_config_flow.update.side_effect = error + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +async def test_homekit_duplicate_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roku_config_flow: MagicMock, +) -> None: + """Test that we handle Homekit flow on duplicates.""" + mock_config_entry.add_to_hass(hass) + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info + ) + + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_ssdp_cannot_connect( - hass: HomeAssistant, mock_roku_config_flow: MagicMock -) -> None: - """Test we abort SSDP flow on connection error.""" - mock_roku_config_flow.update.side_effect = RokuConnectionError - - discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_SSDP}, - data=discovery_info, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - -async def test_ssdp_unknown_error( - hass: HomeAssistant, mock_roku_config_flow: MagicMock -) -> None: - """Test we abort SSDP flow on unknown error.""" - mock_roku_config_flow.update.side_effect = Exception - - discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_SSDP}, - data=discovery_info, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unknown" - - async def test_ssdp_discovery( hass: HomeAssistant, mock_roku_config_flow: MagicMock, mock_setup_entry: None, ) -> None: """Test the SSDP discovery flow.""" - discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + discovery_info = MOCK_SSDP_DISCOVERY_INFO result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -253,16 +262,69 @@ async def test_ssdp_discovery( result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={} ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME + assert "result" in result + assert result["result"].unique_id == "1GU48T017973" + assert result["data"] assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME +@pytest.mark.parametrize( + ("error", "reason"), + [ + (RokuConnectionError, "cannot_connect"), + (RokuConnectionTimeoutError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_ssdp_error( + hass: HomeAssistant, + mock_roku_config_flow: MagicMock, + error: Exception, + reason: str, +) -> None: + """Test we abort SSDP flow on error.""" + mock_roku_config_flow.update.side_effect = error + + discovery_info = MOCK_SSDP_DISCOVERY_INFO + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +async def test_ssdp_duplicate_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roku_config_flow: MagicMock, +) -> None: + """Test that we handle SSDP flow on duplicates.""" + mock_config_entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_options_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: @@ -271,6 +333,8 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" @@ -279,6 +343,8 @@ async def test_options_flow( user_input={CONF_PLAY_MEDIA_APP_ID: "782875"}, ) + await hass.async_block_till_done() + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_PLAY_MEDIA_APP_ID: "782875", @@ -312,6 +378,8 @@ async def test_reconfigure_flow( """Test reconfigure flow.""" result = await _start_reconfigure_flow(hass, mock_config_entry) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -334,5 +402,7 @@ async def test_reconfigure_unique_id_mismatch( result = await _start_reconfigure_flow(hass, mock_config_entry) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_device" diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index 9c414bcf62a..8c810513ef5 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -3,13 +3,30 @@ from unittest.mock import AsyncMock, MagicMock, patch from rokuecp import RokuConnectionError +from syrupy import SnapshotAssertion +from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, + init_integration: MockConfigEntry, +) -> None: + """Test device registry integration.""" + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, init_integration.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + @patch( "homeassistant.components.roku.coordinator.Roku._request", side_effect=RokuConnectionError, diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index d6d5c3f5549..ea0562ced9a 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -57,7 +57,6 @@ from homeassistant.core import HomeAssistant from homeassistant.core_config import async_process_ha_core_config from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from . import setup_integration @@ -109,24 +108,19 @@ async def test_availability( error: RokuError, ) -> None: """Test entity availability.""" - now = dt_util.utcnow() - future = now + timedelta(minutes=1) - mock_config_entry.add_to_hass(hass) - freezer.move_to(now) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - freezer.move_to(future) + freezer.tick(timedelta(minutes=1)) mock_roku.update.side_effect = error - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE - future += timedelta(minutes=1) - freezer.move_to(future) + freezer.tick(timedelta(minutes=1)) mock_roku.update.side_effect = None - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(MAIN_ENTITY_ID).state == STATE_IDLE