diff --git a/homeassistant/components/myuplink/config_flow.py b/homeassistant/components/myuplink/config_flow.py index 15bff643185..cf0428f59ce 100644 --- a/homeassistant/components/myuplink/config_flow.py +++ b/homeassistant/components/myuplink/config_flow.py @@ -6,7 +6,11 @@ from typing import Any import jwt -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + ConfigFlowResult, +) from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, OAUTH2_SCOPES @@ -48,6 +52,12 @@ class OAuth2FlowHandler( return await self.async_step_user() + async def async_step_reconfigure( + self, user_input: Mapping[str, Any] | None = None + ) -> ConfigFlowResult: + """User initiated reconfiguration.""" + return await self.async_step_user() + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create or update the config entry.""" @@ -62,5 +72,10 @@ class OAuth2FlowHandler( return self.async_update_reload_and_abort( self._get_reauth_entry(), data=data ) + if self.source == SOURCE_RECONFIGURE: + self._abort_if_unique_id_mismatch(reason="account_mismatch") + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), data=data + ) self._abort_if_unique_id_configured() return await super().async_oauth_create_entry(data) diff --git a/homeassistant/components/myuplink/quality_scale.yaml b/homeassistant/components/myuplink/quality_scale.yaml index 661986a2f71..463002b5519 100644 --- a/homeassistant/components/myuplink/quality_scale.yaml +++ b/homeassistant/components/myuplink/quality_scale.yaml @@ -82,7 +82,7 @@ rules: status: todo comment: PR pending review \#191937 icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: | diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json index bd60a3c7bb3..d3d2f198448 100644 --- a/homeassistant/components/myuplink/strings.json +++ b/homeassistant/components/myuplink/strings.json @@ -23,6 +23,7 @@ "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "account_mismatch": "The used account does not match the original account", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" }, diff --git a/tests/components/myuplink/test_config_flow.py b/tests/components/myuplink/test_config_flow.py index e823402bda6..0b8d0dba17a 100644 --- a/tests/components/myuplink/test_config_flow.py +++ b/tests/components/myuplink/test_config_flow.py @@ -181,3 +181,96 @@ async def test_flow_reauth_abort( assert result.get("reason") == expected_reason assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.parametrize( + ("unique_id", "scope", "expected_reason"), + [ + ( + UNIQUE_ID, + CURRENT_SCOPE, + "reconfigure_successful", + ), + ( + "wrong_uid", + CURRENT_SCOPE, + "account_mismatch", + ), + ], + ids=["reauth_only", "account_mismatch"], +) +async def test_flow_reconfigure_abort( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + setup_credentials: None, + mock_config_entry: MockConfigEntry, + access_token: str, + expires_at: float, + unique_id: str, + scope: str, + expected_reason: str, +) -> None: + """Test reauth step with correct params and mismatches.""" + + CURRENT_TOKEN = { + "auth_implementation": DOMAIN, + "token": { + "access_token": access_token, + "scope": scope, + "expires_in": 86399, + "refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f", + "token_type": "Bearer", + "expires_at": expires_at, + }, + } + assert hass.config_entries.async_update_entry( + mock_config_entry, data=CURRENT_TOKEN, unique_id=unique_id + ) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URL, + }, + ) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={REDIRECT_URL}" + f"&state={state}" + f"&scope={CURRENT_SCOPE.replace(' ', '+')}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "updated-refresh-token", + "access_token": access_token, + "type": "Bearer", + "expires_in": "60", + "scope": CURRENT_SCOPE, + }, + ) + + with patch( + f"homeassistant.components.{DOMAIN}.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == expected_reason + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1