diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index f3fbd3e0610..3bf3bc82dc1 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from . import async_control_connect, update_client_key +from . import async_control_connect from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS from .helpers import async_get_sources @@ -53,14 +53,11 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - errors: dict[str, str] = {} if user_input is not None: self._host = user_input[CONF_HOST] return await self.async_step_pairing() - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) async def async_step_pairing( self, user_input: dict[str, Any] | None = None @@ -69,13 +66,13 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: self._host}) self.context["title_placeholders"] = {"name": self._name} - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: client = await async_control_connect(self._host, None) except WebOsTvPairError: - return self.async_abort(reason="error_pairing") + errors["base"] = "error_pairing" except WEBOSTV_EXCEPTIONS: errors["base"] = "cannot_connect" else: @@ -130,20 +127,56 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + if user_input is not None: try: client = await async_control_connect(self._host, None) except WebOsTvPairError: - return self.async_abort(reason="error_pairing") + errors["base"] = "error_pairing" except WEBOSTV_EXCEPTIONS: - return self.async_abort(reason="reauth_unsuccessful") + errors["base"] = "cannot_connect" + else: + reauth_entry = self._get_reauth_entry() + data = {CONF_HOST: self._host, CONF_CLIENT_SECRET: client.client_key} + return self.async_update_reload_and_abort(reauth_entry, data=data) - reauth_entry = self._get_reauth_entry() - update_client_key(self.hass, reauth_entry, client) - await self.hass.config_entries.async_reload(reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") + return self.async_show_form(step_id="reauth_confirm", errors=errors) - return self.async_show_form(step_id="reauth_confirm") + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconfigure_entry = self._get_reconfigure_entry() + + if user_input is not None: + host = user_input[CONF_HOST] + client_key = reconfigure_entry.data.get(CONF_CLIENT_SECRET) + + try: + client = await async_control_connect(host, client_key) + except WebOsTvPairError: + errors["base"] = "error_pairing" + except WEBOSTV_EXCEPTIONS: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(client.hello_info["deviceUUID"]) + self._abort_if_unique_id_mismatch(reason="wrong_device") + data = {CONF_HOST: host, CONF_CLIENT_SECRET: client.client_key} + return self.async_update_reload_and_abort(reconfigure_entry, data=data) + + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=reconfigure_entry.data.get(CONF_HOST) + ): cv.string + } + ), + errors=errors, + ) class OptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/webostv/quality_scale.yaml b/homeassistant/components/webostv/quality_scale.yaml index 22c0b4155ab..3a31c20f256 100644 --- a/homeassistant/components/webostv/quality_scale.yaml +++ b/homeassistant/components/webostv/quality_scale.yaml @@ -7,9 +7,7 @@ rules: status: exempt comment: The integration does not use common patterns. config-flow-test-coverage: done - config-flow: - status: todo - comment: make reauth flow more graceful + config-flow: done dependency-transparency: done docs-actions: status: todo @@ -66,7 +64,7 @@ rules: icon-translations: status: exempt comment: The only entity can use the device class. - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: The integration does not have anything to repair. diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index 34c1b44e195..b0786bd06de 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -8,7 +8,7 @@ "host": "[%key:common::config_flow::data::host%]" }, "data_description": { - "host": "Hostname or IP address of your webOS TV." + "host": "Hostname or IP address of your LG webOS TV." } }, "pairing": { @@ -18,17 +18,26 @@ "reauth_confirm": { "title": "[%key:component::webostv::config::step::pairing::title%]", "description": "[%key:component::webostv::config::step::pairing::description%]" + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::webostv::config::step::user::data_description::host%]" + } } }, "error": { - "cannot_connect": "Failed to connect, please turn on your TV or check the IP address" + "cannot_connect": "Failed to connect, please turn on your TV and try again.", + "error_pairing": "Pairing failed, make sure to accept the pairing request on the TV and try again." }, "abort": { - "error_pairing": "Connected to LG webOS TV but not paired", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "wrong_device": "The configured device is not the same found on this Hostname or IP address." } }, "options": { @@ -38,6 +47,9 @@ "description": "Select enabled sources", "data": { "sources": "Sources list" + }, + "data_description": { + "sources": "List of sources to enable" } } }, diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 608e3bd306a..c8ac54be4bd 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -1,7 +1,5 @@ """Test the WebOS Tv config flow.""" -from unittest.mock import AsyncMock - from aiowebostv import WebOsTvPairError import pytest @@ -105,7 +103,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None """Test options config flow cannot retrieve sources.""" entry = await setup_webostv(hass) - client.connect = AsyncMock(side_effect=ConnectionRefusedError()) + client.connect.side_effect = ConnectionRefusedError result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -113,7 +111,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None assert result["errors"] == {"base": "cannot_retrieve"} # recover - client.connect = AsyncMock(return_value=True) + client.connect.side_effect = None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=None, @@ -139,7 +137,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - client.connect = AsyncMock(side_effect=ConnectionRefusedError()) + client.connect.side_effect = ConnectionRefusedError result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -148,7 +146,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: assert result["errors"] == {"base": "cannot_connect"} # recover - client.connect = AsyncMock(return_value=True) + client.connect.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -165,13 +163,22 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - client.connect = AsyncMock(side_effect=WebOsTvPairError("error")) + client.connect.side_effect = WebOsTvPairError result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "error_pairing" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "error_pairing"} + + # recover + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TV_NAME async def test_entry_already_configured(hass: HomeAssistant, client) -> None: @@ -267,9 +274,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: assert entry.data[CONF_HOST] == "new_host" -async def test_reauth_successful( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_reauth_successful(hass: HomeAssistant, client) -> None: """Test that the reauthorization is successful.""" entry = await setup_webostv(hass) @@ -282,7 +287,7 @@ async def test_reauth_successful( assert result["step_id"] == "reauth_confirm" assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY - monkeypatch.setattr(client, "client_key", "new_key") + client.client_key = "new_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -293,15 +298,13 @@ async def test_reauth_successful( @pytest.mark.parametrize( - ("side_effect", "reason"), + ("side_effect", "error"), [ (WebOsTvPairError, "error_pairing"), - (ConnectionRefusedError, "reauth_unsuccessful"), + (ConnectionRefusedError, "cannot_connect"), ], ) -async def test_reauth_errors( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch, side_effect, reason -) -> None: +async def test_reauth_errors(hass: HomeAssistant, client, side_effect, error) -> None: """Test reauthorization errors.""" entry = await setup_webostv(hass) @@ -318,5 +321,88 @@ async def test_reauth_errors( result["flow_id"], user_input={} ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason + assert result["reason"] == "reauth_successful" + + +async def test_reconfigure_successful(hass: HomeAssistant, client) -> None: + """Test that the reconfigure is successful.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data[CONF_HOST] == "new_host" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (WebOsTvPairError, "error_pairing"), + (ConnectionRefusedError, "cannot_connect"), + ], +) +async def test_reconfigure_errors( + hass: HomeAssistant, client, side_effect, error +) -> None: + """Test reconfigure errors.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + client.connect.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_wrong_device(hass: HomeAssistant, client) -> None: + """Test abort if reconfigure host is wrong webOS TV device.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + client.hello_info = {"deviceUUID": "wrong_uuid"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index ba755d80b30..cd8f443c8fd 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -1,9 +1,6 @@ """The tests for the LG webOS TV platform.""" -from unittest.mock import Mock - from aiowebostv import WebOsTvPairError -import pytest from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN @@ -15,12 +12,10 @@ from . import setup_webostv from .const import ENTITY_ID -async def test_reauth_setup_entry( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_reauth_setup_entry(hass: HomeAssistant, client) -> None: """Test reauth flow triggered by setup entry.""" - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + client.is_connected.return_value = False + client.connect.side_effect = WebOsTvPairError entry = await setup_webostv(hass) assert entry.state is ConfigEntryState.SETUP_ERROR @@ -37,11 +32,9 @@ async def test_reauth_setup_entry( assert flow["context"].get("entry_id") == entry.entry_id -async def test_key_update_setup_entry( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_key_update_setup_entry(hass: HomeAssistant, client) -> None: """Test key update from setup entry.""" - monkeypatch.setattr(client, "client_key", "new_key") + client.client_key = "new_key" entry = await setup_webostv(hass) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index 2f29281a496..b12cd0c7c6c 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -1,6 +1,6 @@ """The tests for the WebOS TV notify platform.""" -from unittest.mock import Mock, call +from unittest.mock import call from aiowebostv import WebOsTvPairError import pytest @@ -74,14 +74,12 @@ async def test_notify(hass: HomeAssistant, client) -> None: ) -async def test_notify_not_connected( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_notify_not_connected(hass: HomeAssistant, client) -> None: """Test sending a message when client is not connected.""" await setup_webostv(hass) assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_NAME) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) + client.is_connected.return_value = False await hass.services.async_call( NOTIFY_DOMAIN, SERVICE_NAME, @@ -99,16 +97,13 @@ async def test_notify_not_connected( async def test_icon_not_found( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - client, - monkeypatch: pytest.MonkeyPatch, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client ) -> None: """Test notify icon not found error.""" await setup_webostv(hass) assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_NAME) - monkeypatch.setattr(client, "send_message", Mock(side_effect=FileNotFoundError)) + client.send_message.side_effect = FileNotFoundError await hass.services.async_call( NOTIFY_DOMAIN, SERVICE_NAME, @@ -134,19 +129,14 @@ async def test_icon_not_found( ], ) async def test_connection_errors( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - client, - monkeypatch: pytest.MonkeyPatch, - side_effect, - error, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client, side_effect, error ) -> None: """Test connection errors scenarios.""" await setup_webostv(hass) assert hass.services.has_service("notify", SERVICE_NAME) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) + client.is_connected.return_value = False + client.connect.side_effect = side_effect await hass.services.async_call( NOTIFY_DOMAIN, SERVICE_NAME, @@ -159,7 +149,7 @@ async def test_connection_errors( blocking=True, ) assert client.mock_calls[0] == call.connect() - assert client.connect.call_count == 1 + assert client.connect.call_count == 2 client.send_message.assert_not_called() assert error in caplog.text