diff --git a/homeassistant/components/watts/config_flow.py b/homeassistant/components/watts/config_flow.py index 620d376cfec..aa79f24857e 100644 --- a/homeassistant/components/watts/config_flow.py +++ b/homeassistant/components/watts/config_flow.py @@ -6,7 +6,11 @@ from typing import Any from visionpluspython.auth import WattsVisionAuth -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 @@ -52,6 +56,18 @@ class OAuth2FlowHandler( } ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + return await self.async_step_pick_implementation( + user_input={ + "implementation": self._get_reconfigure_entry().data[ + "auth_implementation" + ] + } + ) + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the OAuth2 flow.""" @@ -64,13 +80,21 @@ class OAuth2FlowHandler( await self.async_set_unique_id(user_id) if self.source == SOURCE_REAUTH: - self._abort_if_unique_id_mismatch(reason="reauth_account_mismatch") + self._abort_if_unique_id_mismatch(reason="account_mismatch") 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 self.async_create_entry( diff --git a/homeassistant/components/watts/manifest.json b/homeassistant/components/watts/manifest.json index 25135798cb2..f1e32b8c503 100644 --- a/homeassistant/components/watts/manifest.json +++ b/homeassistant/components/watts/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["application_credentials", "cloud"], "documentation": "https://www.home-assistant.io/integrations/watts", "iot_class": "cloud_polling", - "quality_scale": "silver", + "quality_scale": "platinum", "requirements": ["visionpluspython==1.0.2"] } diff --git a/homeassistant/components/watts/quality_scale.yaml b/homeassistant/components/watts/quality_scale.yaml index 349cd8ced16..c42cee4a798 100644 --- a/homeassistant/components/watts/quality_scale.yaml +++ b/homeassistant/components/watts/quality_scale.yaml @@ -60,7 +60,7 @@ rules: icon-translations: status: exempt comment: Thermostat entities use standard HA Climate entity. - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: No actionable repair scenarios, auth issues are handled by reauthentication flow. diff --git a/homeassistant/components/watts/strings.json b/homeassistant/components/watts/strings.json index aeea7abfd83..9f1c761d8f7 100644 --- a/homeassistant/components/watts/strings.json +++ b/homeassistant/components/watts/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "account_mismatch": "The authenticated account does not match the configured account", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", @@ -12,8 +13,8 @@ "oauth_implementation_unavailable": "[%key:common::config_flow::abort::oauth2_implementation_unavailable%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "reauth_account_mismatch": "The authenticated account does not match the account that needed re-authentication", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" }, "create_entry": { diff --git a/tests/components/watts/test_config_flow.py b/tests/components/watts/test_config_flow.py index 67c9fbf64a6..bbb31d6a5ad 100644 --- a/tests/components/watts/test_config_flow.py +++ b/tests/components/watts/test_config_flow.py @@ -297,7 +297,100 @@ async def test_reauth_account_mismatch( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_account_mismatch" + assert result["reason"] == "account_mismatch" + + +@pytest.mark.usefixtures("current_request_with_host", "mock_setup_entry") +async def test_reconfigure_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the reconfiguration flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "new-refresh-token", + "access_token": "new-access-token", + "token_type": "Bearer", + "expires_in": 3600, + }, + ) + + with patch( + "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", + return_value="test-user-id", + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + mock_config_entry.data["token"].pop("expires_at") + assert mock_config_entry.data["token"] == { + "refresh_token": "new-refresh-token", + "access_token": "new-access-token", + "token_type": "Bearer", + "expires_in": 3600, + } + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_reconfigure_account_mismatch( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguration with a different account aborts.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "new-refresh-token", + "access_token": "new-access-token", + "token_type": "Bearer", + "expires_in": 3600, + }, + ) + + with patch( + "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", + return_value="different-user-id", + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "account_mismatch" @pytest.mark.usefixtures("current_request_with_host")