diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 4ed39d1cc146e5..47aa32dcd11cde 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -79,13 +79,6 @@ def __init__( self._device_flow_info: DeviceFlowInfo = device_flow_info self._exchange_task_unsub: CALLBACK_TYPE | None = None self._timeout_unsub: CALLBACK_TYPE | None = None - max_timeout = dt.utcnow() + datetime.timedelta(seconds=EXCHANGE_TIMEOUT_SECONDS) - # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime - # object without tzinfo. For the comparison below to work, it needs one. - user_code_expiry = device_flow_info.user_code_expiry.replace( - tzinfo=datetime.timezone.utc - ) - self._expiration_time = min(user_code_expiry, max_timeout) self._listener: CALLBACK_TYPE | None = None self._creds: Credentials | None = None @@ -115,13 +108,21 @@ def creds(self) -> Credentials | None: def async_start_exchange(self) -> None: """Start the device auth exchange flow polling.""" _LOGGER.debug("Starting exchange flow") + max_timeout = dt.utcnow() + datetime.timedelta(seconds=EXCHANGE_TIMEOUT_SECONDS) + # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime + # object without tzinfo. For the comparison below to work, it needs one. + user_code_expiry = self._device_flow_info.user_code_expiry.replace( + tzinfo=datetime.timezone.utc + ) + expiration_time = min(user_code_expiry, max_timeout) + self._exchange_task_unsub = async_track_time_interval( self._hass, self._async_poll_attempt, datetime.timedelta(seconds=self._device_flow_info.interval), ) self._timeout_unsub = async_track_point_in_utc_time( - self._hass, self._async_timeout, self._expiration_time + self._hass, self._async_timeout, expiration_time ) async def _async_poll_attempt(self, now: datetime.datetime) -> None: diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 3d18faae47169c..24ad8a7b76911a 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -10,6 +10,7 @@ from aiohttp.client_exceptions import ClientError from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( + DeviceFlowInfo, FlowExchangeError, OAuth2Credentials, OAuth2DeviceCodeError, @@ -59,10 +60,17 @@ async def mock_code_flow( ) -> YieldFixture[Mock]: """Fixture for initiating OAuth flow.""" with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", ) as mock_flow: - mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta - mock_flow.return_value.interval = CODE_CHECK_INTERVAL + mock_flow.return_value = DeviceFlowInfo.FromResponse( + { + "device_code": "4/4-GMMhmHCXhWEzkobqIHGG_EnNYYsAkukHspeYUk9E8", + "user_code": "GQVQ-JKEC", + "verification_url": "https://www.google.com/device", + "expires_in": code_expiration_delta.total_seconds(), + "interval": CODE_CHECK_INTERVAL, + } + ) yield mock_flow @@ -70,7 +78,8 @@ async def mock_code_flow( async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: """Fixture for mocking out the exchange for credentials.""" with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", + return_value=creds, ) as mock: yield mock @@ -108,7 +117,6 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, @@ -131,9 +139,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) - await fire_alarm(hass, datetime.datetime.utcnow()) - await hass.async_block_till_done() + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) @@ -143,11 +150,12 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data + assert 0 < data["token"]["expires_in"] <= 60 * 60 assert ( - data["token"]["expires_in"] - == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() + datetime.datetime.now().timestamp() + <= data["token"]["expires_at"] + < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() ) - assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { @@ -238,7 +246,7 @@ async def test_code_error( assert await component_setup() with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", side_effect=OAuth2DeviceCodeError("Test Failure"), ): result = await hass.config_entries.flow.async_init( @@ -267,7 +275,7 @@ async def test_expired_after_exchange( # Fail first attempt then advance clock past exchange timeout with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", side_effect=FlowExchangeError(), ): now = utcnow() @@ -299,7 +307,7 @@ async def test_exchange_error( # Run one tick to invoke the credential exchange check now = utcnow() with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", side_effect=FlowExchangeError(), ): now += CODE_CHECK_ALARM_TIMEDELTA