@@ -271,40 +271,40 @@ async def test_get_auth_credential_raises_error_if_upstream_call_fails(
271271@patch .object (_iam_connector_credentials_provider .time , "time" )
272272async def test_get_auth_credential_raises_error_if_polling_times_out (
273273 mock_time ,
274- mock_operation ,
274+ mock_client ,
275275 auth_scheme ,
276276 context ,
277277 provider ,
278278):
279279 """Test get_auth_credential raises RuntimeError if polling times out."""
280280
281- # Force the operation into the polling loop state
281+ # 1. Setup the operation metadata to indicate consent is pending
282282 meta_pb = RetrieveCredentialsMetadata .pb ()()
283283 meta_pb .consent_pending .SetInParent ()
284- meta = RetrieveCredentialsMetadata .deserialize (meta_pb .SerializeToString ())
285- mock_operation .metadata .value = RetrieveCredentialsMetadata .serialize (meta )
286284
287- # First call sets start_time=0.0, second call checks time > timeout
288- # (20.0 > 10.0 )
289- mock_time . side_effect = [ 0.0 , 20.0 ]
285+ # 2. Keep the operation in the pending state (done=False)
286+ op_pending = Operation ( done = False )
287+ op_pending . metadata . value = meta_pb . SerializeToString ()
290288
291- mock_metadata = Mock ( spec = RetrieveCredentialsMetadata )
292- mock_metadata . consent_pending = True
293- mock_metadata . uri_consent_required = False
294- mock_operation . done = True
295- mock_operation . ClearField ( "error" )
296- mock_client = Mock ( spec = Client )
297- mock_client . retrieve_credentials . side_effect = Exception (
298- "Timeout waiting for credentials."
299- )
300- provider . _client = mock_client
289+ # Configure the mock client to repeatedly return this pending operation
290+ mock_client . retrieve_credentials . return_value = Mock ( operation = op_pending )
291+
292+ # 3. Simulate the passage of time:
293+ # - 1st time.time() sets start/end time (returns 0.0 )
294+ # - 2nd time.time() inside the while-loop check returns a value exceeding the
295+ # NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC timeout (currently 10.0s)
296+ provider_lib = _iam_connector_credentials_provider
297+ timeout = provider_lib . NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC
298+ mock_time . side_effect = [ 0.0 , timeout + 10.0 ]
301299
300+ # 4. Verify that timeout raises RuntimeError wrapping TimeoutError
302301 with pytest .raises (
303302 RuntimeError ,
304303 match = "Failed to retrieve credential for user 'user' on connector" ,
305304 ) as exc_info :
306305 await provider .get_auth_credential (auth_scheme , context )
307306
307+ # Assert that the underlying cause was indeed a TimeoutError from polling
308308 assert "Timeout waiting for credentials." in str (exc_info .value .__cause__ )
309309
310310
@@ -508,3 +508,55 @@ async def test_get_auth_credential_handles_consent_pending_state_correctly(
508508
509509 # Verify that retrieve_credentials was called twice (initial + 1 poll)
510510 assert mock_client .retrieve_credentials .call_count == 2
511+
512+
513+ @patch .object (_iam_connector_credentials_provider .time , "time" )
514+ @patch .object (_iam_connector_credentials_provider .asyncio , "sleep" )
515+ async def test_get_auth_credential_polling_succeeds_before_timeout (
516+ mock_sleep ,
517+ mock_time ,
518+ mock_client ,
519+ auth_scheme ,
520+ context ,
521+ provider ,
522+ ):
523+ """Test get_auth_credential returns credential if polling succeeds."""
524+ # 1. Setup the first retrieve_credentials call to return a pending Operation
525+ # containing consent_pending metadata.
526+ meta_pb = RetrieveCredentialsMetadata .pb ()()
527+ meta_pb .consent_pending .SetInParent ()
528+
529+ op_pending = Operation (done = False )
530+ op_pending .metadata .value = meta_pb .SerializeToString ()
531+
532+ # 2. Setup the operation to succeed on a subsequent poll.
533+ op_success = Operation (done = True )
534+ resp = RetrieveCredentialsResponse (
535+ header = "Authorization: Bearer" , token = "valid-token"
536+ )
537+ op_success .response .value = RetrieveCredentialsResponse .serialize (resp )
538+
539+ # Configure mock client to return pending, pending, then success.
540+ mock_client .retrieve_credentials .side_effect = [
541+ Mock (operation = op_pending ),
542+ Mock (operation = op_pending ),
543+ Mock (operation = op_success ),
544+ ]
545+
546+ # 3. Simulate the passage of time:
547+ # - 1st time.time() sets end_time (returns 0.0)
548+ # - 2nd time.time() inside loop (returns 0.0, first poll -> pending)
549+ # - 3rd time.time() inside loop (returns timeout / 5.0, 2nd poll -> success)
550+ provider_lib = _iam_connector_credentials_provider
551+ timeout = provider_lib .NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC
552+ mock_time .side_effect = [0.0 , 0.0 , timeout / 5.0 ]
553+
554+ # 4. Call the provider and verify.
555+ credential = await provider .get_auth_credential (auth_scheme , context )
556+
557+ assert credential is not None
558+ assert credential .http .credentials .token == "valid-token"
559+ assert mock_client .retrieve_credentials .call_count == 3
560+ mock_sleep .assert_called_once_with (
561+ provider_lib .NON_INTERACTIVE_TOKEN_POLL_INTERVAL_SEC
562+ )
0 commit comments