diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 678bf17d1a3e..c90dded67f4e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -149,9 +149,23 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle initiation of re-authentication with Tuya.""" if CONF_USER_CODE in entry_data: - success, _ = await self.__async_get_qr_code(entry_data[CONF_USER_CODE]) - if success: - return await self.async_step_scan() + self.__user_code = entry_data[CONF_USER_CODE] + return await self.async_step_reauth_confirm() + + return await self.async_step_reauth_user_code() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm re-authentication with Tuya.""" + # The QR code expires in minutes, while a reauth flow can sit untouched + # for days. Request the code when the user is ready to scan it. + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + + success, _ = await self.__async_get_qr_code(self.__user_code) + if success: + return await self.async_step_scan() return await self.async_step_reauth_user_code() diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index bc93406bc4e7..8b0c8dd60f38 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -5,6 +5,10 @@ "login_error": "Login error ({code}): {msg}" }, "step": { + "reauth_confirm": { + "description": "Your Smart Life or Tuya Smart account needs to be reauthenticated.\n\nSelect **Submit** to get a QR code to scan with the app.", + "title": "[%key:common::config_flow::title::reauth%]" + }, "reauth_user_code": { "data": { "user_code": "User code" diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index d9bb632eee9f..c46a8dfc719f 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -134,9 +134,9 @@ async def test_user_flow_failed_scan( assert result4.get("type") is FlowResultType.CREATE_ENTRY -@pytest.mark.usefixtures("mock_tuya_login_control") async def test_reauth_flow( hass: HomeAssistant, + mock_tuya_login_control: MagicMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: @@ -146,19 +146,66 @@ async def test_reauth_flow( result = await mock_config_entry.start_reauth_flow(hass) assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "scan" + assert result.get("step_id") == "reauth_confirm" + + # A reauth flow can sit untouched for days, so the QR code must not be + # requested before the user is ready to scan it. + mock_tuya_login_control.qr_code.assert_not_called() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result2.get("type") is FlowResultType.ABORT - assert result2.get("reason") == "reauth_successful" + assert result2.get("type") is FlowResultType.FORM + assert result2.get("step_id") == "scan" + mock_tuya_login_control.qr_code.assert_called_once() + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result3.get("type") is FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" assert mock_config_entry == snapshot +async def test_reauth_flow_without_user_code( + hass: HomeAssistant, + mock_tuya_login_control: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauthentication of an entry that predates the stored user code.""" + mock_config_entry.add_to_hass(hass) + data = dict(mock_config_entry.data) + del data[CONF_USER_CODE] + hass.config_entries.async_update_entry(mock_config_entry, data=data) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "reauth_user_code" + mock_tuya_login_control.qr_code.assert_not_called() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_USER_CODE: "12345"}, + ) + + assert result2.get("type") is FlowResultType.FORM + assert result2.get("step_id") == "scan" + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result3.get("type") is FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" + + async def test_reauth_flow_failed_qr_code( hass: HomeAssistant, mock_tuya_login_control: MagicMock, @@ -173,15 +220,23 @@ async def test_reauth_flow_failed_qr_code( result = await mock_config_entry.start_reauth_flow(hass) assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "reauth_user_code" + assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2.get("type") is FlowResultType.FORM + assert result2.get("step_id") == "reauth_user_code" + + result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") is FlowResultType.FORM - assert result2.get("errors") == {"base": "login_error"} + assert result3.get("type") is FlowResultType.FORM + assert result3.get("errors") == {"base": "login_error"} # This time it worked out mock_tuya_login_control.qr_code.return_value["success"] = True