@@ -853,3 +853,109 @@ async def slow_refresh(*args, **kwargs):
853853 assert all (r .status_code == 200 for r in results )
854854 assert refresh_count == 1
855855 await session .close ()
856+
857+ @pytest .mark .asyncio
858+ async def test_cert_rotation_with_completed_mtls_init_task ():
859+ """
860+ Verifies that when _mtls_init_task is already completed, the
861+ _mtls_init_task.done() reset branch in configure_mtls_channel() is exercised.
862+ """
863+ old_cert = b"old_cert_data"
864+ new_cert = b"new_cert_data"
865+ new_key = b"new_key_data"
866+
867+ mock_creds = mock .AsyncMock (spec = credentials .Credentials )
868+
869+ # Mock a completed initial task
870+ async def dummy_init ():
871+ return None
872+ completed_task = asyncio .create_task (dummy_init ())
873+ await completed_task
874+
875+ # Mock response returning 401 first, then 200 after rotation
876+ mock_auth_request = mock .AsyncMock (spec = transport .Request )
877+ mock_resp_401 = mock .Mock (spec = transport .Response , status_code = 401 )
878+ mock_resp_200 = mock .Mock (spec = transport .Response , status_code = 200 )
879+ mock_auth_request .side_effect = [mock_resp_401 , mock_resp_200 ]
880+
881+ session = sessions .AsyncAuthorizedSession (
882+ mock_creds , auth_request = mock_auth_request
883+ )
884+ session ._is_mtls = True
885+ session ._cached_cert = old_cert
886+ session ._mtls_init_task = completed_task # Pre-populate completed task
887+
888+ with mock .patch (
889+ "google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response" ,
890+ return_value = (new_cert , new_key , "old_fp" , "new_fp" ),
891+ ), mock .patch .object (
892+ session , "configure_mtls_channel" , wraps = session .configure_mtls_channel
893+ ) as spy_configure :
894+ resp = await session .request ("GET" , "https://example.com" )
895+
896+ assert resp .status_code == 200
897+ assert spy_configure .called
898+ # Verify the previous task was replaced and new configuration completed
899+ assert session ._mtls_init_task is not completed_task
900+ assert session ._mtls_init_task .done ()
901+
902+ await session .close ()
903+
904+ @pytest .mark .asyncio
905+ async def test_401_retry_raises_timeout_before_refresh ():
906+ """
907+ Tests that TimeoutError is raised if max_allowed_time expires before
908+ credentials.refresh can be executed following a 401.
909+ """
910+ mock_creds = mock .AsyncMock (spec = credentials .Credentials )
911+ mock_auth_request = mock .AsyncMock (spec = transport .Request )
912+ mock_resp_401 = mock .Mock (spec = transport .Response , status_code = 401 )
913+ mock_auth_request .return_value = mock_resp_401
914+
915+ session = sessions .AsyncAuthorizedSession (
916+ mock_creds , auth_request = mock_auth_request
917+ )
918+
919+ # Initial monotonic call: start (0), before_request (0.2), request (0.4)
920+ # Then monotonic advances to 10.0 (exceeding max_allowed_time=1.0) before refresh
921+ with mock .patch ("time.monotonic" , side_effect = [0 , 0.2 , 0.4 , 10.0 , 10.0 , 10.0 ]):
922+ with pytest .raises (exceptions .TimeoutError ):
923+ await session .request ("GET" , "https://example.com" , max_allowed_time = 1.0 )
924+
925+ # Assert refresh was never called because timeout expired beforehand
926+ mock_creds .refresh .assert_not_called ()
927+ await session .close ()
928+
929+ @pytest .mark .asyncio
930+ async def test_401_retry_raises_timeout_before_subsequent_retry ():
931+ """
932+ Tests that TimeoutError is raised if credentials.refresh succeeds on 401,
933+ but remaining time expires before the retry request can complete.
934+ """
935+ mock_creds = mock .AsyncMock (spec = credentials .Credentials )
936+ mock_auth_request = mock .AsyncMock (spec = transport .Request )
937+ mock_resp_401 = mock .Mock (spec = transport .Response , status_code = 401 )
938+ mock_auth_request .return_value = mock_resp_401
939+
940+ async def mock_refresh (auth_request ):
941+ # Refresh takes time
942+ return None
943+
944+ mock_creds .refresh = mock .AsyncMock (side_effect = mock_refresh )
945+
946+ session = sessions .AsyncAuthorizedSession (
947+ mock_creds , auth_request = mock_auth_request
948+ )
949+
950+ # Simulate monotonic advancing so refresh completes but remaining time <= 0 for the retry
951+ with mock .patch (
952+ "time.monotonic" ,
953+ side_effect = [0.0 , 0.1 , 0.2 , 0.3 , 0.4 , 5.0 , 5.0 , 5.0 ],
954+ ):
955+ with pytest .raises (exceptions .TimeoutError ):
956+ await session .request ("GET" , "https://example.com" , max_allowed_time = 1.0 )
957+
958+ # Assert refresh was called once, but the subsequent request aborted on timeout
959+ assert mock_creds .refresh .call_count == 1
960+ assert mock_auth_request .call_count == 1
961+ await session .close ()
0 commit comments