From c553bce2023b199613f65ca0b45fdbd288f0da99 Mon Sep 17 00:00:00 2001 From: Nik Rahmel Date: Tue, 21 Jul 2026 14:34:42 +0100 Subject: [PATCH] Fix Tado reusing an expired device code when retrying after login timeout (#176832) Co-authored-by: Claude Fable 5 Co-authored-by: Erwin Douna --- homeassistant/components/tado/config_flow.py | 3 +- tests/components/tado/test_config_flow.py | 60 ++++++++++++++------ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 6d22cb777f22..a57b07f87b59 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -167,7 +167,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="timeout", ) - del self.login_task + self.login_task = None + self.tado = None return await self.async_step_user() @override diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 5367dcc43773..3e7cec5a17f5 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -110,26 +110,54 @@ async def test_auth_timeout( mock_tado_api: MagicMock, mock_setup_entry: AsyncMock, ) -> None: - """Test the auth timeout.""" - mock_tado_api.device_activation_status.return_value = DeviceActivationStatus.PENDING - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + """Test that retrying after an auth timeout uses a fresh device code.""" + expired_client = MagicMock() + expired_client.device_verification_url.return_value = ( + "https://login.tado.com/oauth2/device?user_code=EXPIRED" ) - assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE - assert result["step_id"] == "timeout" - - mock_tado_api.device_activation_status.return_value = ( - DeviceActivationStatus.COMPLETED + expired_client.device_activation_status.return_value = ( + DeviceActivationStatus.PENDING ) - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "timeout" + event = threading.Event() - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + def mock_tado_api_device_activation() -> None: + # Simulate the device activation process + event.wait(timeout=5) + + mock_tado_api.device_activation = mock_tado_api_device_activation + + with patch( + "homeassistant.components.tado.config_flow.Tado", + side_effect=[expired_client, mock_tado_api], + ) as mock_tado_create: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "timeout" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "timeout" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + # The retry must construct a new client and show its device code, + # not the expired one from the first attempt + assert mock_tado_create.call_count == 2 + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["description_placeholders"] == { + "url": "https://login.tado.com/oauth2/device?user_code=TEST", + "code": "TEST", + } + + event.set() + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home name"