Skip to content

Commit 94ee6c3

Browse files
Add tests for MTLS certificate rotation and timeouts
1 parent 0a6057a commit 94ee6c3

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

packages/google-auth/tests/transport/aio/test_sessions_mtls.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)