chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,23 @@
# Integration tests for GCP Agent Identity Credentials service
Verifies OAuth flows using GCP Agent Identity Credentials service.
## Setup
To set up your environment for the first time, create a virtual environment
and install dependencies:
```bash
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
uv sync --all-extras
```
Then, install test specific packages
```bash
pip install google-cloud-iamconnectorcredentials
```
## Run Tests
```bash
pytest -s tests/integration/integrations/agent_identity
```
@@ -0,0 +1,268 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""E2E Integration Test for GCP Agent Identity Auth Provider two-legged OAuth Flow."""
import dataclasses
from typing import Any
from unittest import mock
from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_authenticated_tool import BaseAuthenticatedTool
from google.adk.tools.mcp_tool.mcp_tool import McpTool
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsRequest
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
from google.genai import types
from mcp.types import Tool as McpBaseTool
import pytest
from tests.unittests import testing_utils
DUMMY_TOKEN = "fake-gcp-2lo-token-123"
TEST_CONNECTOR_2LO = (
"projects/test-project/locations/global/connectors/test-connector"
)
class DummyTool(BaseAuthenticatedTool):
def __init__(self, auth_config: AuthConfig) -> None:
super().__init__(
name="dummy_tool",
description="Dummy tool for testing 2LO.",
auth_config=auth_config,
)
def _get_declaration(self) -> types.FunctionDeclaration:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
parameters=types.Schema(
type="OBJECT",
properties={},
),
)
async def _run_async_impl(
self, *, args: dict[str, Any] | None, tool_context: Any, credential: Any
) -> Any:
# Return the token to prove the provider gave the expected credential
if credential.http and credential.http.credentials:
return credential.http.credentials.token
if credential.oauth2 and credential.oauth2.access_token:
return credential.oauth2.access_token
return None
@dataclasses.dataclass
class _DummyOperation:
done: bool = True
error: Any = None
metadata: Any = None
response: Any = dataclasses.field(init=False)
operation: Any = dataclasses.field(init=False)
def __post_init__(self) -> None:
self.response = mock.Mock()
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token=DUMMY_TOKEN
)
self.response.value = RetrieveCredentialsResponse.serialize(mock_credential)
self.operation = self
def HasField(self, field_name: str) -> bool:
return getattr(self, field_name, None) is not None
# Mocked execution; pin to a single LLM backend to avoid duplicate runs.
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_gets_token() -> None:
"""Test the end-to-end flow fetching 2LO OAuth token from GCP Agent Identity credentials service."""
# Clear registry to isolate tests
CredentialManager._auth_provider_registry._providers.clear()
# 1. Setup mocked GCP Client to return the fake Bearer token
with mock.patch.object(
_iam_connector_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
mock_operation = _DummyOperation()
mock_client_cls.return_value.retrieve_credentials.return_value = (
mock_operation
)
# 2. Configure Auth and DummyTool
auth_scheme = GcpAuthProviderScheme(
name=TEST_CONNECTOR_2LO,
scopes=["test-scope"],
)
auth_config = AuthConfig(auth_scheme=auth_scheme)
dummy_tool = DummyTool(auth_config=auth_config)
# 3. Setup LLM, Agent, and Runner
# We mock the LLM to just issue the tool call to 'dummy_tool'
mock_model = testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_tool", args={}),
"Tool executed successfully.",
]
)
agent = Agent(
name="test_agent",
model=mock_model,
instruction="You are an agent. Use the dummy_tool when needed.",
tools=[dummy_tool],
)
runner = Runner(
app_name="test_mcp_2lo_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
)
# 4. Register Auth Provider
CredentialManager.register_auth_provider(GcpAuthProvider())
# 5. Execute Flow
event_list = []
async for event in runner.run_async(
user_id="test_user",
session_id="test_session1",
new_message=types.UserContent(
parts=[types.Part(text="Get me the token.")]
),
):
event_list.append(event)
# 6. Assertions
# Assert GCP Agent Identity client was invoked for credentials
expected_request = RetrieveCredentialsRequest(
connector=TEST_CONNECTOR_2LO,
user_id="test_user",
scopes=["test-scope"],
continue_uri="",
force_refresh=False,
)
mock_client_cls.return_value.retrieve_credentials.assert_called_once_with(
expected_request
)
# 3 Events: Model FunctionCall -> Tool FunctionResponse -> Final LLM Text
assert len(event_list) == 3
last_event = event_list[-1]
assert last_event.content.parts[0].text == "Tool executed successfully."
# Validate that the mock model received the query and the tool callback
requests = mock_model.requests
# 2 Events: User Input -> Tool FunctionResponse
assert len(requests) == 2
# Extract the function response from the prompt payload sent to the LLM
last_request = requests[-1]
function_response = next(
(
p.function_response
for p in last_request.contents[-1].parts
if p.function_response
),
None,
)
assert function_response.name == "dummy_tool"
assert DUMMY_TOKEN in str(function_response.response)
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_sends_authorization_header_to_mcp_session(
llm_backend: Any,
) -> None:
"""Ensures a 2LO token from GCP is correctly passed into the outbound MCP session headers."""
CredentialManager._auth_provider_registry._providers.clear()
CredentialManager.register_auth_provider(GcpAuthProvider())
mock_operation = _DummyOperation()
with mock.patch.object(
_iam_connector_credentials_provider, "Client", autospec=True
) as mock_gcp:
mock_gcp.return_value.retrieve_credentials.return_value = mock_operation
mock_session_mgr = mock.AsyncMock()
mock_session_mgr.create_session.return_value.call_tool.return_value = (
mock.MagicMock()
)
mcp_tool = McpTool(
mcp_tool=McpBaseTool(
name="dummy_mcp",
description="Dummy MCP tool for testing.",
inputSchema={"type": "object", "properties": {}},
),
mcp_session_manager=mock_session_mgr,
auth_scheme=GcpAuthProviderScheme(
name=TEST_CONNECTOR_2LO, scopes=["test-scope"]
),
)
agent = Agent(
name="test_agent",
model=testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_mcp", args={}),
"Tool executed successfully.",
]
),
instruction="Use dummy_mcp tool.",
tools=[mcp_tool],
)
async for _ in Runner(
app_name="test_mcp_header_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
).run_async(
user_id="test_user",
session_id="session-id-2",
new_message=types.UserContent(parts=[types.Part(text="Run tool.")]),
):
pass
mock_gcp.return_value.retrieve_credentials.assert_called_once_with(
RetrieveCredentialsRequest(
connector=TEST_CONNECTOR_2LO,
user_id="test_user",
scopes=["test-scope"],
force_refresh=False,
)
)
assert mock_session_mgr.create_session.call_args.kwargs.get("headers") == {
"Authorization": f"Bearer {DUMMY_TOKEN}"
}
@@ -0,0 +1,295 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""E2E Integration Test for 3LO flow using GCP Agent Identity service."""
import dataclasses
from typing import Any
from unittest import mock
from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_authenticated_tool import BaseAuthenticatedTool
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsMetadata
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsRequest
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
from google.genai import types
import pytest
from tests.unittests import testing_utils
DUMMY_TOKEN = "mock-token-3legged"
TEST_CONNECTOR_3LO = (
"projects/my-project/locations/some-location/connectors/test-connector-3lo"
)
class DummyTool(BaseAuthenticatedTool):
def __init__(self, auth_config: AuthConfig) -> None:
super().__init__(
name="dummy_tool",
description="Dummy tool for testing 3LO.",
auth_config=auth_config,
)
def _get_declaration(self) -> types.FunctionDeclaration:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
parameters=types.Schema(
type="OBJECT",
properties={},
),
)
async def _run_async_impl(
self, *, args: dict[str, Any] | None, tool_context: Any, credential: Any
) -> Any:
# Extract and return the token to prove the provider gave us the expected credential
if credential.http and credential.http.credentials:
return credential.http.credentials.token
if credential.oauth2 and credential.oauth2.access_token:
return credential.oauth2.access_token
return None
@dataclasses.dataclass
class _MockOperation:
done: bool
response_obj: Any = None
metadata_obj: Any = None
error: Any = None
metadata: Any = dataclasses.field(init=False, default=None)
response: Any = dataclasses.field(init=False, default=None)
operation: Any = dataclasses.field(init=False)
def __post_init__(self) -> None:
if self.metadata_obj:
self.metadata = mock.Mock()
self.metadata.value = RetrieveCredentialsMetadata.serialize(
self.metadata_obj
)
if self.response_obj:
self.response = mock.Mock()
self.response.value = RetrieveCredentialsResponse.serialize(
self.response_obj
)
self.operation = self
def HasField(self, field_name: str) -> bool:
return getattr(self, field_name, None) is not None
class MockGcpClient:
"""Lightweight in-memory mock for Agent Identity Credentials service 3LO Consent Flow."""
def __init__(self) -> None:
self.finalized_connectors = set()
def retrieve_credentials(
self,
request: RetrieveCredentialsRequest | dict[str, Any] | None = None,
**kwargs: Any,
) -> _MockOperation:
connector = (
request.get("connector")
if isinstance(request, dict)
else getattr(request, "connector", None)
)
if connector in self.finalized_connectors:
mock_credential = RetrieveCredentialsResponse(
token=DUMMY_TOKEN, header="Authorization: Bearer"
)
return _MockOperation(done=True, response_obj=mock_credential)
# Otherwise, return Consent Required
# Auto-finalize for the next call to simulate user approval flow
self.finalized_connectors.add(connector)
mock_metadata = RetrieveCredentialsMetadata(
uri_consent_required=RetrieveCredentialsMetadata.UriConsentRequired(
authorization_uri="http://mock-auth-uri",
consent_nonce="mock-consent-nonce",
)
)
return _MockOperation(done=False, metadata_obj=mock_metadata)
# Mocked execution; pin to a single LLM backend to avoid duplicate runs.
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_3lo_user_consent_flow() -> None:
# Clear registry to isolate tests
CredentialManager._auth_provider_registry._providers.clear()
# 1. Setup mocked GCP Client to simulate stateful 3LO process
mock_gcp_client = MockGcpClient()
with mock.patch.object(
_iam_connector_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
mock_client_cls.return_value.retrieve_credentials.side_effect = (
mock_gcp_client.retrieve_credentials
)
# 2. Configure Auth and DummyTool
auth_scheme = GcpAuthProviderScheme(
name=TEST_CONNECTOR_3LO,
scopes=["test-scope"],
continue_uri="https://example.com/continue",
)
auth_config = AuthConfig(auth_scheme=auth_scheme)
dummy_tool = DummyTool(auth_config=auth_config)
# 3. Setup LLM, Agent, and Runner
# We mock the LLM to just issue the tool call to 'dummy_tool'
mock_model = testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_tool", args={}),
"Tool executed successfully.",
]
)
agent = Agent(
name="test_agent",
model=mock_model,
instruction="You are an agent. Use the dummy_tool when needed.",
tools=[dummy_tool],
)
runner = Runner(
app_name="test_mcp_3lo_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
)
# 4. Register Auth Provider
CredentialManager.register_auth_provider(GcpAuthProvider())
# 5. Execute Flow
session = await runner.session_service.create_session(
app_name="test_mcp_3lo_app", user_id="test_user"
)
event_list = []
# Step 5a: User sends message, Agent requests credential
async for event in runner.run_async(
user_id="test_user",
session_id=session.id,
new_message=types.UserContent(
parts=[types.Part(text="Get me the token.")]
),
):
event_list.append(event)
def _find_auth_request_event(events):
for event in events:
for part in event.content.parts:
if (
part.function_call
and part.function_call.name == "adk_request_credential"
):
return event
return None
auth_request_event = _find_auth_request_event(event_list)
assert (
auth_request_event
), "Expected adk_request_credential tool call not found."
# Step 5b: Simulate User Consent
call_part = next(
p for p in auth_request_event.content.parts if p.function_call
)
request_auth_config = call_part.function_call.args.get("authConfig", {})
assert (
request_auth_config.get("exchangedAuthCredential", {})
.get("oauth2", {})
.get("nonce")
== "mock-consent-nonce"
)
# Step 5c: User acknowledges credential request
response_part = types.Part.from_function_response(
name="adk_request_credential", response=request_auth_config
)
response_part.function_response.id = call_part.function_call.id
final_response_parts = []
async for event in runner.run_async(
user_id="test_user",
session_id=session.id,
new_message=types.UserContent(parts=[response_part]),
):
event_list.append(event)
if event.content:
for part in event.content.parts:
if part.text:
final_response_parts.append(part.text)
final_response_text = "".join(final_response_parts)
# 6. Assertions
# Assert GCP Agent Identity client was invoked for credentials twice
# (Initial Request + Post-Consent call)
assert mock_client_cls.return_value.retrieve_credentials.call_count == 2
expected_request = RetrieveCredentialsRequest(
connector=TEST_CONNECTOR_3LO,
user_id="test_user",
scopes=["test-scope"],
continue_uri="https://example.com/continue",
force_refresh=False,
)
mock_client_cls.return_value.retrieve_credentials.assert_called_with(
expected_request
)
assert "Tool executed successfully." in final_response_text
# Validate requests received by the mock model
requests = mock_model.requests
# Two LLM calls: the tool call in turn 1 (which ends at the EUC) and the
# post-consent response in turn 2.
assert len(requests) == 2
# Extract the function response from the prompt payload sent to the LLM
last_request = requests[-1]
function_response = next(
(
p.function_response
for p in last_request.contents[-1].parts
if p.function_response
),
None,
)
assert function_response is not None
assert function_response.name == "dummy_tool"
assert DUMMY_TOKEN in str(function_response.response)
@@ -0,0 +1,263 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""E2E Integration Test for GCP Agent Identity Auth Provider two-legged OAuth Flow."""
import dataclasses
from typing import Any
from unittest import mock
from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import _agent_identity_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_authenticated_tool import BaseAuthenticatedTool
from google.adk.tools.mcp_tool.mcp_tool import McpTool
from google.cloud.agentidentitycredentials_v1 import RetrieveCredentialsRequest
from google.genai import types
from mcp.types import Tool as McpBaseTool
import pytest
from tests.unittests import testing_utils
DUMMY_TOKEN = "fake-gcp-2lo-token-123"
TEST_AUTH_PROVIDER_2LO = (
"projects/test-project/locations/global/authProviders/test-provider"
)
class DummyTool(BaseAuthenticatedTool):
def __init__(self, auth_config: AuthConfig) -> None:
super().__init__(
name="dummy_tool",
description="Dummy tool for testing 2LO.",
auth_config=auth_config,
)
def _get_declaration(self) -> types.FunctionDeclaration:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
parameters=types.Schema(
type="OBJECT",
properties={},
),
)
async def _run_async_impl(
self, *, args: dict[str, Any] | None, tool_context: Any, credential: Any
) -> Any:
# Return the token to prove the provider gave the expected credential
if credential.http and credential.http.credentials:
return credential.http.credentials.token
if credential.oauth2 and credential.oauth2.access_token:
return credential.oauth2.access_token
return None
@dataclasses.dataclass
class _DummyOperation:
done: bool = True
error: Any = None
metadata: Any = None
response: Any = dataclasses.field(init=False)
success: Any = dataclasses.field(init=False)
def __post_init__(self) -> None:
self.success = mock.Mock()
self.success.header = "Authorization: Bearer"
self.success.token = DUMMY_TOKEN
self.response = self
def __contains__(self, key: str) -> bool:
return key == "success"
# Mocked execution; pin to a single LLM backend to avoid duplicate runs.
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_gets_token() -> None:
"""Test the end-to-end flow fetching 2LO OAuth token from GCP Agent Identity credentials service."""
# Clear registry to isolate tests
CredentialManager._auth_provider_registry._providers.clear()
# 1. Setup mocked GCP Client to return the fake Bearer token
with mock.patch.object(
_agent_identity_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
mock_operation = _DummyOperation()
mock_client_cls.return_value.retrieve_credentials.return_value = (
mock_operation
)
# 2. Configure Auth and DummyTool
auth_scheme = GcpAuthProviderScheme(
name=TEST_AUTH_PROVIDER_2LO,
scopes=["test-scope"],
)
auth_config = AuthConfig(auth_scheme=auth_scheme)
dummy_tool = DummyTool(auth_config=auth_config)
# 3. Setup LLM, Agent, and Runner
# We mock the LLM to just issue the tool call to 'dummy_tool'
mock_model = testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_tool", args={}),
"Tool executed successfully.",
]
)
agent = Agent(
name="test_agent",
model=mock_model,
instruction="You are an agent. Use the dummy_tool when needed.",
tools=[dummy_tool],
)
runner = Runner(
app_name="test_mcp_2lo_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
)
# 4. Register Auth Provider
CredentialManager.register_auth_provider(GcpAuthProvider())
# 5. Execute Flow
event_list = []
async for event in runner.run_async(
user_id="test_user",
session_id="test_session1",
new_message=types.UserContent(
parts=[types.Part(text="Get me the token.")]
),
):
event_list.append(event)
# 6. Assertions
# Assert GCP Agent Identity client was invoked for credentials
expected_request = RetrieveCredentialsRequest(
auth_provider=TEST_AUTH_PROVIDER_2LO,
user_id="test_user",
scopes=["test-scope"],
continue_uri="",
)
mock_client_cls.return_value.retrieve_credentials.assert_called_once_with(
expected_request
)
# 3 Events: Model FunctionCall -> Tool FunctionResponse -> Final LLM Text
assert len(event_list) == 3
last_event = event_list[-1]
assert last_event.content.parts[0].text == "Tool executed successfully."
# Validate that the mock model received the query and the tool callback
requests = mock_model.requests
# 2 Events: User Input -> Tool FunctionResponse
assert len(requests) == 2
# Extract the function response from the prompt payload sent to the LLM
last_request = requests[-1]
function_response = next(
(
p.function_response
for p in last_request.contents[-1].parts
if p.function_response
),
None,
)
assert function_response.name == "dummy_tool"
assert DUMMY_TOKEN in str(function_response.response)
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_sends_authorization_header_to_mcp_session(
llm_backend: Any,
) -> None:
"""Ensures a 2LO token from GCP is correctly passed into the outbound MCP session headers."""
CredentialManager._auth_provider_registry._providers.clear()
CredentialManager.register_auth_provider(GcpAuthProvider())
mock_operation = _DummyOperation()
with mock.patch.object(
_agent_identity_credentials_provider, "Client", autospec=True
) as mock_gcp:
mock_gcp.return_value.retrieve_credentials.return_value = mock_operation
mock_session_mgr = mock.AsyncMock()
mock_session_mgr.create_session.return_value.call_tool.return_value = (
mock.MagicMock()
)
mcp_tool = McpTool(
mcp_tool=McpBaseTool(
name="dummy_mcp",
description="Dummy MCP tool for testing.",
inputSchema={"type": "object", "properties": {}},
),
mcp_session_manager=mock_session_mgr,
auth_scheme=GcpAuthProviderScheme(
name=TEST_AUTH_PROVIDER_2LO, scopes=["test-scope"]
),
)
agent = Agent(
name="test_agent",
model=testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_mcp", args={}),
"Tool executed successfully.",
]
),
instruction="Use dummy_mcp tool.",
tools=[mcp_tool],
)
async for _ in Runner(
app_name="test_mcp_header_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
).run_async(
user_id="test_user",
session_id="session-id-2",
new_message=types.UserContent(parts=[types.Part(text="Run tool.")]),
):
pass
mock_gcp.return_value.retrieve_credentials.assert_called_once_with(
RetrieveCredentialsRequest(
auth_provider=TEST_AUTH_PROVIDER_2LO,
user_id="test_user",
scopes=["test-scope"],
)
)
assert mock_session_mgr.create_session.call_args.kwargs.get("headers") == {
"Authorization": f"Bearer {DUMMY_TOKEN}"
}
@@ -0,0 +1,285 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""E2E Integration Test for 3LO flow using GCP Agent Identity service."""
import dataclasses
from typing import Any
from unittest import mock
from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import _agent_identity_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_authenticated_tool import BaseAuthenticatedTool
from google.cloud.agentidentitycredentials_v1 import RetrieveCredentialsRequest
from google.genai import types
import pytest
from tests.unittests import testing_utils
DUMMY_TOKEN = "mock-token-3legged"
TEST_AUTH_PROVIDER_3LO = "projects/my-project/locations/some-location/authProviders/test-provider-3lo"
class DummyTool(BaseAuthenticatedTool):
def __init__(self, auth_config: AuthConfig) -> None:
super().__init__(
name="dummy_tool",
description="Dummy tool for testing 3LO.",
auth_config=auth_config,
)
def _get_declaration(self) -> types.FunctionDeclaration:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
parameters=types.Schema(
type="OBJECT",
properties={},
),
)
async def _run_async_impl(
self, *, args: dict[str, Any] | None, tool_context: Any, credential: Any
) -> Any:
# Extract and return the token to prove the provider gave us the expected credential
if credential.http and credential.http.credentials:
return credential.http.credentials.token
if credential.oauth2 and credential.oauth2.access_token:
return credential.oauth2.access_token
return None
@dataclasses.dataclass
class _MockOperation:
done: bool
response_obj: Any = None
metadata_obj: Any = None
error: Any = None
response: Any = dataclasses.field(init=False, default=None)
success: Any = dataclasses.field(init=False, default=None)
uri_consent_required: Any = dataclasses.field(init=False, default=None)
def __post_init__(self) -> None:
if self.metadata_obj:
self.uri_consent_required = self.metadata_obj.uri_consent_required
if self.response_obj:
self.success = mock.Mock()
self.success.header = self.response_obj.header
self.success.token = self.response_obj.token
self.response = self
def __contains__(self, key: str) -> bool:
return getattr(self, key, None) is not None
class MockGcpClient:
"""Lightweight in-memory mock for Agent Identity Credentials service 3LO Consent Flow."""
def __init__(self) -> None:
self.finalized_connectors = set()
def retrieve_credentials(
self,
request: RetrieveCredentialsRequest | dict[str, Any] | None = None,
**kwargs: Any,
) -> _MockOperation:
auth_provider = (
request.get("auth_provider")
if isinstance(request, dict)
else getattr(request, "auth_provider", None)
)
if auth_provider in self.finalized_connectors:
mock_credential = mock.Mock(
token=DUMMY_TOKEN, header="Authorization: Bearer"
)
return _MockOperation(done=True, response_obj=mock_credential)
# Otherwise, return Consent Required
# Auto-finalize for the next call to simulate user approval flow
self.finalized_connectors.add(auth_provider)
mock_metadata = mock.Mock()
mock_metadata.uri_consent_required = mock.Mock(
authorization_uri="http://mock-auth-uri",
consent_nonce="mock-consent-nonce",
)
return _MockOperation(done=False, metadata_obj=mock_metadata)
# Mocked execution; pin to a single LLM backend to avoid duplicate runs.
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_3lo_user_consent_flow() -> None:
# Clear registry to isolate tests
CredentialManager._auth_provider_registry._providers.clear()
# 1. Setup mocked GCP Client to simulate stateful 3LO process
mock_gcp_client = MockGcpClient()
with mock.patch.object(
_agent_identity_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
mock_client_cls.return_value.retrieve_credentials.side_effect = (
mock_gcp_client.retrieve_credentials
)
# 2. Configure Auth and DummyTool
auth_scheme = GcpAuthProviderScheme(
name=TEST_AUTH_PROVIDER_3LO,
scopes=["test-scope"],
continue_uri="https://example.com/continue",
)
auth_config = AuthConfig(auth_scheme=auth_scheme)
dummy_tool = DummyTool(auth_config=auth_config)
# 3. Setup LLM, Agent, and Runner
# We mock the LLM to just issue the tool call to 'dummy_tool'
mock_model = testing_utils.MockModel.create(
responses=[
types.Part.from_function_call(name="dummy_tool", args={}),
"Tool executed successfully.",
]
)
agent = Agent(
name="test_agent",
model=mock_model,
instruction="You are an agent. Use the dummy_tool when needed.",
tools=[dummy_tool],
)
runner = Runner(
app_name="test_mcp_3lo_app",
agent=agent,
session_service=InMemorySessionService(),
auto_create_session=True,
)
# 4. Register Auth Provider
CredentialManager.register_auth_provider(GcpAuthProvider())
# 5. Execute Flow
session = await runner.session_service.create_session(
app_name="test_mcp_3lo_app", user_id="test_user"
)
event_list = []
# Step 5a: User sends message, Agent requests credential
async for event in runner.run_async(
user_id="test_user",
session_id=session.id,
new_message=types.UserContent(
parts=[types.Part(text="Get me the token.")]
),
):
event_list.append(event)
def _find_auth_request_event(events):
for event in events:
for part in event.content.parts:
if (
part.function_call
and part.function_call.name == "adk_request_credential"
):
return event
return None
auth_request_event = _find_auth_request_event(event_list)
assert (
auth_request_event
), "Expected adk_request_credential tool call not found."
# Step 5b: Simulate User Consent
call_part = next(
p for p in auth_request_event.content.parts if p.function_call
)
request_auth_config = call_part.function_call.args.get("authConfig", {})
assert (
request_auth_config.get("exchangedAuthCredential", {})
.get("oauth2", {})
.get("nonce")
== "mock-consent-nonce"
)
# Step 5c: User acknowledges credential request
response_part = types.Part.from_function_response(
name="adk_request_credential", response=request_auth_config
)
response_part.function_response.id = call_part.function_call.id
final_response_parts = []
async for event in runner.run_async(
user_id="test_user",
session_id=session.id,
new_message=types.UserContent(parts=[response_part]),
):
event_list.append(event)
if event.content:
for part in event.content.parts:
if part.text:
final_response_parts.append(part.text)
final_response_text = "".join(final_response_parts)
# 6. Assertions
# Assert GCP Agent Identity client was invoked for credentials twice
# (Initial Request + Post-Consent call)
assert mock_client_cls.return_value.retrieve_credentials.call_count == 2
expected_request = RetrieveCredentialsRequest(
auth_provider=TEST_AUTH_PROVIDER_3LO,
user_id="test_user",
scopes=["test-scope"],
continue_uri="https://example.com/continue",
)
mock_client_cls.return_value.retrieve_credentials.assert_called_with(
expected_request
)
assert "Tool executed successfully." in final_response_text
# Validate requests received by the mock model
requests = mock_model.requests
# Two LLM calls: the tool call in turn 1 (which ends at the EUC) and the
# post-consent response in turn 2.
assert len(requests) == 2
# Extract the function response from the prompt payload sent to the LLM
last_request = requests[-1]
function_response = next(
(
p.function_response
for p in last_request.contents[-1].parts
if p.function_response
),
None,
)
assert function_response is not None
assert function_response.name == "dummy_tool"
assert DUMMY_TOKEN in str(function_response.response)