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

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,13 @@
# 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.
@@ -0,0 +1,759 @@
# 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.
from unittest.mock import MagicMock
from unittest.mock import patch
from google.adk.tools.google_api_tool.googleapi_to_openapi_converter import GoogleApiToOpenApiConverter
import pytest
@pytest.fixture
def docs_api_spec():
"""Fixture that provides a mock Google Docs API spec for testing."""
return {
"kind": "discovery#restDescription",
"id": "docs:v1",
"name": "docs",
"version": "v1",
"title": "Google Docs API",
"description": "Reads and writes Google Docs documents.",
"documentationLink": "https://developers.google.com/docs/",
"protocol": "rest",
"rootUrl": "https://docs.googleapis.com/",
"servicePath": "",
"auth": {
"oauth2": {
"scopes": {
"https://www.googleapis.com/auth/documents": {
"description": (
"See, edit, create, and delete all of your Google"
" Docs documents"
)
},
"https://www.googleapis.com/auth/documents.readonly": {
"description": "View your Google Docs documents"
},
"https://www.googleapis.com/auth/drive": {
"description": (
"See, edit, create, and delete all of your Google"
" Drive files"
)
},
"https://www.googleapis.com/auth/drive.file": {
"description": (
"View and manage Google Drive files and folders that"
" you have opened or created with this app"
)
},
}
}
},
"schemas": {
"Document": {
"type": "object",
"description": "A Google Docs document",
"properties": {
"documentId": {
"type": "string",
"description": "The ID of the document",
},
"title": {
"type": "string",
"description": "The title of the document",
},
"body": {"$ref": "Body", "description": "The document body"},
"revisionId": {
"type": "string",
"description": "The revision ID of the document",
},
},
},
"Body": {
"type": "object",
"description": "The document body",
"properties": {
"content": {
"type": "array",
"description": "The content of the body",
"items": {"$ref": "StructuralElement"},
}
},
},
"StructuralElement": {
"type": "object",
"description": "A structural element of a document",
"properties": {
"startIndex": {
"type": "integer",
"description": "The zero-based start index",
},
"endIndex": {
"type": "integer",
"description": "The zero-based end index",
},
},
},
"BatchUpdateDocumentRequest": {
"type": "object",
"description": "Request to batch update a document",
"properties": {
"requests": {
"type": "array",
"description": (
"A list of updates to apply to the document"
),
"items": {"$ref": "Request"},
},
"writeControl": {
"$ref": "WriteControl",
"description": (
"Provides control over how write requests are"
" executed"
),
},
},
},
"Request": {
"type": "object",
"description": "A single kind of update to apply to a document",
"properties": {
"insertText": {"$ref": "InsertTextRequest"},
"updateTextStyle": {"$ref": "UpdateTextStyleRequest"},
"replaceAllText": {"$ref": "ReplaceAllTextRequest"},
},
},
"InsertTextRequest": {
"type": "object",
"description": "Inserts text into the document",
"properties": {
"location": {
"$ref": "Location",
"description": "The location to insert text",
},
"text": {
"type": "string",
"description": "The text to insert",
},
},
},
"UpdateTextStyleRequest": {
"type": "object",
"description": "Updates the text style of the specified range",
"properties": {
"range": {
"$ref": "Range",
"description": "The range to update",
},
"textStyle": {
"$ref": "TextStyle",
"description": "The text style to apply",
},
"fields": {
"type": "string",
"description": "The fields that should be updated",
},
},
},
"ReplaceAllTextRequest": {
"type": "object",
"description": "Replaces all instances of text matching criteria",
"properties": {
"containsText": {"$ref": "SubstringMatchCriteria"},
"replaceText": {
"type": "string",
"description": (
"The text that will replace the matched text"
),
},
},
},
"Location": {
"type": "object",
"description": "A particular location in the document",
"properties": {
"index": {
"type": "integer",
"description": "The zero-based index",
},
"tabId": {
"type": "string",
"description": "The tab the location is in",
},
},
},
"Range": {
"type": "object",
"description": "Specifies a contiguous range of text",
"properties": {
"startIndex": {
"type": "integer",
"description": "The zero-based start index",
},
"endIndex": {
"type": "integer",
"description": "The zero-based end index",
},
},
},
"TextStyle": {
"type": "object",
"description": (
"Represents the styling that can be applied to text"
),
"properties": {
"bold": {
"type": "boolean",
"description": "Whether or not the text is bold",
},
"italic": {
"type": "boolean",
"description": "Whether or not the text is italic",
},
"fontSize": {
"$ref": "Dimension",
"description": "The size of the text's font",
},
},
},
"SubstringMatchCriteria": {
"type": "object",
"description": (
"A criteria that matches a specific string of text in the"
" document"
),
"properties": {
"text": {
"type": "string",
"description": "The text to search for",
},
"matchCase": {
"type": "boolean",
"description": (
"Indicates whether the search should respect case"
),
},
},
},
"WriteControl": {
"type": "object",
"description": (
"Provides control over how write requests are executed"
),
"properties": {
"requiredRevisionId": {
"type": "string",
"description": "The required revision ID",
},
"targetRevisionId": {
"type": "string",
"description": "The target revision ID",
},
},
},
"BatchUpdateDocumentResponse": {
"type": "object",
"description": "Response from a BatchUpdateDocument request",
"properties": {
"documentId": {
"type": "string",
"description": "The ID of the document",
},
"replies": {
"type": "array",
"description": "The reply of the updates",
"items": {"$ref": "Response"},
},
"writeControl": {
"$ref": "WriteControl",
"description": "The updated write control",
},
},
},
"Response": {
"type": "object",
"description": "A single response from an update",
"properties": {
"replaceAllText": {"$ref": "ReplaceAllTextResponse"},
},
},
"ReplaceAllTextResponse": {
"type": "object",
"description": "The result of replacing text",
"properties": {
"occurrencesChanged": {
"type": "integer",
"description": "The number of occurrences changed",
},
},
},
},
"resources": {
"documents": {
"methods": {
"get": {
"id": "docs.documents.get",
"path": "v1/documents/{documentId}",
"flatPath": "v1/documents/{documentId}",
"httpMethod": "GET",
"description": (
"Gets the latest version of the specified document."
),
"parameters": {
"documentId": {
"type": "string",
"description": (
"The ID of the document to retrieve"
),
"required": True,
"location": "path",
}
},
"response": {"$ref": "Document"},
"scopes": [
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/documents.readonly",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
],
},
"create": {
"id": "docs.documents.create",
"path": "v1/documents",
"httpMethod": "POST",
"description": (
"Creates a blank document using the title given in"
" the request."
),
"request": {"$ref": "Document"},
"response": {"$ref": "Document"},
"scopes": [
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
],
},
"batchUpdate": {
"id": "docs.documents.batchUpdate",
"path": "v1/documents/{documentId}:batchUpdate",
"flatPath": "v1/documents/{documentId}:batchUpdate",
"httpMethod": "POST",
"description": (
"Applies one or more updates to the document."
),
"parameters": {
"documentId": {
"type": "string",
"description": "The ID of the document to update",
"required": True,
"location": "path",
}
},
"request": {"$ref": "BatchUpdateDocumentRequest"},
"response": {"$ref": "BatchUpdateDocumentResponse"},
"scopes": [
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
],
},
},
}
},
}
@pytest.fixture
def docs_converter():
"""Fixture that provides a basic docs converter instance."""
return GoogleApiToOpenApiConverter("docs", "v1")
@pytest.fixture
def mock_docs_api_resource(docs_api_spec):
"""Fixture that provides a mock API resource with the docs test spec."""
mock_resource = MagicMock()
mock_resource._rootDesc = docs_api_spec
return mock_resource
@pytest.fixture
def prepared_docs_converter(docs_converter, docs_api_spec):
"""Fixture that provides a converter with the Docs API spec already set."""
docs_converter._google_api_spec = docs_api_spec
return docs_converter
@pytest.fixture
def docs_converter_with_patched_build(monkeypatch, mock_docs_api_resource):
"""Fixture that provides a converter with the build function patched.
This simulates a successful API spec fetch.
"""
# Create a mock for the build function
mock_build = MagicMock(return_value=mock_docs_api_resource)
# Patch the build function in the target module
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
# Create and return a converter instance
return GoogleApiToOpenApiConverter("docs", "v1")
class TestDocsApiBatchUpdate:
"""Test suite for the Google Docs API batchUpdate endpoint conversion."""
def test_batch_update_method_conversion(
self, prepared_docs_converter, docs_api_spec
):
"""Test conversion of the batchUpdate method specifically."""
# Convert methods from the documents resource
methods = docs_api_spec["resources"]["documents"]["methods"]
prepared_docs_converter._convert_methods(methods, "/v1/documents")
# Verify the results
paths = prepared_docs_converter._openapi_spec["paths"]
# Check that batchUpdate POST method exists
assert "/v1/documents/{documentId}:batchUpdate" in paths
batch_update_method = paths["/v1/documents/{documentId}:batchUpdate"][
"post"
]
# Verify method details
assert batch_update_method["operationId"] == "docs.documents.batchUpdate"
assert (
batch_update_method["summary"]
== "Applies one or more updates to the document."
)
# Check parameters exist
params = batch_update_method["parameters"]
param_names = [p["name"] for p in params]
assert "documentId" in param_names
# Check request body
assert "requestBody" in batch_update_method
request_body = batch_update_method["requestBody"]
assert request_body["required"] is True
request_schema = request_body["content"]["application/json"]["schema"]
assert (
request_schema["$ref"]
== "#/components/schemas/BatchUpdateDocumentRequest"
)
# Check response
assert "responses" in batch_update_method
response_schema = batch_update_method["responses"]["200"]["content"][
"application/json"
]["schema"]
assert (
response_schema["$ref"]
== "#/components/schemas/BatchUpdateDocumentResponse"
)
# Check security/scopes
assert "security" in batch_update_method
# Should have OAuth2 scopes for documents access
def test_batch_update_request_schema_conversion(
self, prepared_docs_converter, docs_api_spec
):
"""Test that BatchUpdateDocumentRequest schema is properly converted."""
# Convert schemas using the actual method signature
prepared_docs_converter._convert_schemas()
schemas = prepared_docs_converter._openapi_spec["components"]["schemas"]
# Check BatchUpdateDocumentRequest schema
assert "BatchUpdateDocumentRequest" in schemas
batch_request_schema = schemas["BatchUpdateDocumentRequest"]
assert batch_request_schema["type"] == "object"
assert "properties" in batch_request_schema
assert "requests" in batch_request_schema["properties"]
assert "writeControl" in batch_request_schema["properties"]
# Check requests array property
requests_prop = batch_request_schema["properties"]["requests"]
assert requests_prop["type"] == "array"
assert requests_prop["items"]["$ref"] == "#/components/schemas/Request"
def test_batch_update_response_schema_conversion(
self, prepared_docs_converter, docs_api_spec
):
"""Test that BatchUpdateDocumentResponse schema is properly converted."""
# Convert schemas using the actual method signature
prepared_docs_converter._convert_schemas()
schemas = prepared_docs_converter._openapi_spec["components"]["schemas"]
# Check BatchUpdateDocumentResponse schema
assert "BatchUpdateDocumentResponse" in schemas
batch_response_schema = schemas["BatchUpdateDocumentResponse"]
assert batch_response_schema["type"] == "object"
assert "properties" in batch_response_schema
assert "documentId" in batch_response_schema["properties"]
assert "replies" in batch_response_schema["properties"]
assert "writeControl" in batch_response_schema["properties"]
# Check replies array property
replies_prop = batch_response_schema["properties"]["replies"]
assert replies_prop["type"] == "array"
assert replies_prop["items"]["$ref"] == "#/components/schemas/Response"
def test_batch_update_request_types_conversion(
self, prepared_docs_converter, docs_api_spec
):
"""Test that various request types are properly converted."""
# Convert schemas using the actual method signature
prepared_docs_converter._convert_schemas()
schemas = prepared_docs_converter._openapi_spec["components"]["schemas"]
# Check Request schema (union of different request types)
assert "Request" in schemas
request_schema = schemas["Request"]
assert "properties" in request_schema
# Should contain different request types
assert "insertText" in request_schema["properties"]
assert "updateTextStyle" in request_schema["properties"]
assert "replaceAllText" in request_schema["properties"]
# Check InsertTextRequest
assert "InsertTextRequest" in schemas
insert_text_schema = schemas["InsertTextRequest"]
assert "location" in insert_text_schema["properties"]
assert "text" in insert_text_schema["properties"]
# Check UpdateTextStyleRequest
assert "UpdateTextStyleRequest" in schemas
update_style_schema = schemas["UpdateTextStyleRequest"]
assert "range" in update_style_schema["properties"]
assert "textStyle" in update_style_schema["properties"]
assert "fields" in update_style_schema["properties"]
def test_convert_methods(self, prepared_docs_converter, docs_api_spec):
"""Test conversion of API methods."""
# Convert methods
methods = docs_api_spec["resources"]["documents"]["methods"]
prepared_docs_converter._convert_methods(methods, "/v1/documents")
# Verify the results
paths = prepared_docs_converter._openapi_spec["paths"]
# Check GET method
assert "/v1/documents/{documentId}" in paths
get_method = paths["/v1/documents/{documentId}"]["get"]
assert get_method["operationId"] == "docs.documents.get"
# Check parameters
params = get_method["parameters"]
param_names = [p["name"] for p in params]
assert "documentId" in param_names
# Check POST method (create)
assert "/v1/documents" in paths
post_method = paths["/v1/documents"]["post"]
assert post_method["operationId"] == "docs.documents.create"
# Check request body
assert "requestBody" in post_method
assert (
post_method["requestBody"]["content"]["application/json"]["schema"][
"$ref"
]
== "#/components/schemas/Document"
)
# Check response
assert (
post_method["responses"]["200"]["content"]["application/json"][
"schema"
]["$ref"]
== "#/components/schemas/Document"
)
# Check batchUpdate POST method
assert "/v1/documents/{documentId}:batchUpdate" in paths
batch_update_method = paths["/v1/documents/{documentId}:batchUpdate"][
"post"
]
assert batch_update_method["operationId"] == "docs.documents.batchUpdate"
def test_complete_docs_api_conversion(
self, docs_converter_with_patched_build
):
"""Integration test for complete Docs API conversion including batchUpdate."""
# Call the method
result = docs_converter_with_patched_build.convert()
# Verify basic structure
assert result["openapi"] == "3.0.0"
assert "info" in result
assert "servers" in result
assert "paths" in result
assert "components" in result
# Verify paths
paths = result["paths"]
assert "/v1/documents/{documentId}" in paths
assert "get" in paths["/v1/documents/{documentId}"]
# Verify batchUpdate endpoint
assert "/v1/documents/{documentId}:batchUpdate" in paths
assert "post" in paths["/v1/documents/{documentId}:batchUpdate"]
# Verify method details
get_document = paths["/v1/documents/{documentId}"]["get"]
assert get_document["operationId"] == "docs.documents.get"
assert "parameters" in get_document
# Verify batchUpdate method
batch_update = paths["/v1/documents/{documentId}:batchUpdate"]["post"]
assert batch_update["operationId"] == "docs.documents.batchUpdate"
# Verify request body
assert "requestBody" in batch_update
request_schema = batch_update["requestBody"]["content"]["application/json"][
"schema"
]
assert (
request_schema["$ref"]
== "#/components/schemas/BatchUpdateDocumentRequest"
)
# Verify response body
assert "responses" in batch_update
response_schema = batch_update["responses"]["200"]["content"][
"application/json"
]["schema"]
assert (
response_schema["$ref"]
== "#/components/schemas/BatchUpdateDocumentResponse"
)
# Verify schemas exist
schemas = result["components"]["schemas"]
assert "Document" in schemas
assert "BatchUpdateDocumentRequest" in schemas
assert "BatchUpdateDocumentResponse" in schemas
assert "InsertTextRequest" in schemas
assert "UpdateTextStyleRequest" in schemas
assert "ReplaceAllTextRequest" in schemas
def test_batch_update_example_request_structure(
self, prepared_docs_converter, docs_api_spec
):
"""Test that the converted schema can represent a realistic batchUpdate request."""
# Convert schemas using the actual method signature
prepared_docs_converter._convert_schemas()
schemas = prepared_docs_converter._openapi_spec["components"]["schemas"]
# Verify that we can represent a realistic batch update request like:
# {
# "requests": [
# {
# "insertText": {
# "location": {"index": 1},
# "text": "Hello World"
# }
# },
# {
# "updateTextStyle": {
# "range": {"startIndex": 1, "endIndex": 6},
# "textStyle": {"bold": true},
# "fields": "bold"
# }
# }
# ],
# "writeControl": {
# "requiredRevisionId": "some-revision-id"
# }
# }
# Check that all required schemas exist for this structure
assert "BatchUpdateDocumentRequest" in schemas
assert "Request" in schemas
assert "InsertTextRequest" in schemas
assert "UpdateTextStyleRequest" in schemas
assert "Location" in schemas
assert "Range" in schemas
assert "TextStyle" in schemas
assert "WriteControl" in schemas
# Verify Location schema has required properties
location_schema = schemas["Location"]
assert "index" in location_schema["properties"]
assert location_schema["properties"]["index"]["type"] == "integer"
# Verify Range schema has required properties
range_schema = schemas["Range"]
assert "startIndex" in range_schema["properties"]
assert "endIndex" in range_schema["properties"]
# Verify TextStyle schema has formatting properties
text_style_schema = schemas["TextStyle"]
assert "bold" in text_style_schema["properties"]
assert text_style_schema["properties"]["bold"]["type"] == "boolean"
def test_integration_docs_api(self, docs_converter_with_patched_build):
"""Integration test using Google Docs API specification."""
# Create and run the converter
openapi_spec = docs_converter_with_patched_build.convert()
# Verify conversion results
assert openapi_spec["info"]["title"] == "Google Docs API"
assert openapi_spec["servers"][0]["url"] == "https://docs.googleapis.com"
# Check security schemes
security_schemes = openapi_spec["components"]["securitySchemes"]
assert "oauth2" in security_schemes
assert "apiKey" in security_schemes
# Check schemas
schemas = openapi_spec["components"]["schemas"]
assert "Document" in schemas
assert "BatchUpdateDocumentRequest" in schemas
assert "BatchUpdateDocumentResponse" in schemas
assert "InsertTextRequest" in schemas
assert "UpdateTextStyleRequest" in schemas
assert "ReplaceAllTextRequest" in schemas
# Check paths
paths = openapi_spec["paths"]
assert "/v1/documents/{documentId}" in paths
assert "/v1/documents" in paths
assert "/v1/documents/{documentId}:batchUpdate" in paths
# Check method details
get_document = paths["/v1/documents/{documentId}"]["get"]
assert get_document["operationId"] == "docs.documents.get"
# Check batchUpdate method details
batch_update = paths["/v1/documents/{documentId}:batchUpdate"]["post"]
assert batch_update["operationId"] == "docs.documents.batchUpdate"
# Check parameter details
param_dict = {p["name"]: p for p in get_document["parameters"]}
assert "documentId" in param_dict
document_id = param_dict["documentId"]
assert document_id["required"] is True
assert document_id["schema"]["type"] == "string"
@@ -0,0 +1,153 @@
# 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.
from unittest import mock
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import ServiceAccount
from google.adk.auth.auth_credential import ServiceAccountCredential
from google.adk.tools.google_api_tool.google_api_tool import GoogleApiTool
from google.adk.tools.openapi_tool import RestApiTool
from google.adk.tools.tool_context import ToolContext
from google.genai.types import FunctionDeclaration
import pytest
@pytest.fixture
def mock_rest_api_tool():
"""Fixture for a mock RestApiTool."""
mock_tool = mock.MagicMock(spec=RestApiTool)
mock_tool.name = "test_tool"
mock_tool.description = "Test Tool Description"
mock_tool.is_long_running = False
mock_tool._get_declaration.return_value = FunctionDeclaration(
name="test_function", description="Test function description"
)
mock_tool.run_async.return_value = {"result": "success"}
return mock_tool
@pytest.fixture
def mock_tool_context():
"""Fixture for a mock ToolContext."""
return mock.MagicMock(spec=ToolContext)
class TestGoogleApiTool:
"""Test suite for the GoogleApiTool class."""
def test_init(self, mock_rest_api_tool):
"""Test GoogleApiTool initialization."""
tool = GoogleApiTool(mock_rest_api_tool)
assert tool.name == "test_tool"
assert tool.description == "Test Tool Description"
assert tool.is_long_running is False
assert tool._rest_api_tool == mock_rest_api_tool
def test_init_with_additional_headers(self, mock_rest_api_tool):
"""Test GoogleApiTool initialization with additional headers."""
headers = {"developer-token": "test-token"}
GoogleApiTool(mock_rest_api_tool, additional_headers=headers)
mock_rest_api_tool.set_default_headers.assert_called_once_with(headers)
def test_get_declaration(self, mock_rest_api_tool):
"""Test _get_declaration method."""
tool = GoogleApiTool(mock_rest_api_tool)
declaration = tool._get_declaration()
assert isinstance(declaration, FunctionDeclaration)
assert declaration.name == "test_function"
assert declaration.description == "Test function description"
mock_rest_api_tool._get_declaration.assert_called_once()
@pytest.mark.asyncio
async def test_run_async(self, mock_rest_api_tool, mock_tool_context):
"""Test run_async method."""
tool = GoogleApiTool(mock_rest_api_tool)
args = {"param1": "value1"}
result = await tool.run_async(args=args, tool_context=mock_tool_context)
assert result == {"result": "success"}
mock_rest_api_tool.run_async.assert_called_once_with(
args=args, tool_context=mock_tool_context
)
def test_configure_auth(self, mock_rest_api_tool):
"""Test configure_auth method."""
tool = GoogleApiTool(mock_rest_api_tool)
client_id = "test_client_id"
client_secret = "test_client_secret"
tool.configure_auth(client_id=client_id, client_secret=client_secret)
# Check that auth_credential was set correctly on the rest_api_tool
assert mock_rest_api_tool.auth_credential is not None
assert (
mock_rest_api_tool.auth_credential.auth_type
== AuthCredentialTypes.OPEN_ID_CONNECT
)
assert mock_rest_api_tool.auth_credential.oauth2.client_id == client_id
assert (
mock_rest_api_tool.auth_credential.oauth2.client_secret == client_secret
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_tool.service_account_scheme_credential"
)
def test_configure_sa_auth(
self, mock_service_account_scheme_credential, mock_rest_api_tool
):
"""Test configure_sa_auth method."""
# Setup mock return values
mock_auth_scheme = mock.MagicMock()
mock_auth_credential = mock.MagicMock()
mock_service_account_scheme_credential.return_value = (
mock_auth_scheme,
mock_auth_credential,
)
service_account = ServiceAccount(
service_account_credential=ServiceAccountCredential(
type="service_account",
project_id="project_id",
private_key_id="private_key_id",
private_key="private_key",
client_email="client_email",
client_id="client_id",
auth_uri="auth_uri",
token_uri="token_uri",
auth_provider_x509_cert_url="auth_provider_x509_cert_url",
client_x509_cert_url="client_x509_cert_url",
universe_domain="universe_domain",
),
scopes=["scope1", "scope2"],
)
# Create tool and call method
tool = GoogleApiTool(mock_rest_api_tool)
tool.configure_sa_auth(service_account=service_account)
# Verify service_account_scheme_credential was called correctly
mock_service_account_scheme_credential.assert_called_once_with(
service_account
)
# Verify auth_scheme and auth_credential were set correctly on the rest_api_tool
assert mock_rest_api_tool.auth_scheme == mock_auth_scheme
assert mock_rest_api_tool.auth_credential == mock_auth_credential
@@ -0,0 +1,610 @@
# 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.
from typing import Optional
from unittest import mock
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.auth.auth_credential import ServiceAccount
from google.adk.auth.auth_credential import ServiceAccountCredential
from google.adk.auth.auth_schemes import OpenIdConnectWithConfig
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.base_toolset import ToolPredicate
from google.adk.tools.google_api_tool.google_api_tool import GoogleApiTool
from google.adk.tools.google_api_tool.google_api_toolset import GoogleApiToolset
from google.adk.tools.google_api_tool.googleapi_to_openapi_converter import GoogleApiToOpenApiConverter
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
import pytest
TEST_API_NAME = "calendar"
TEST_API_VERSION = "v3"
DEFAULT_SCOPE = "https://www.googleapis.com/auth/calendar"
@pytest.fixture
def mock_rest_api_tool():
"""Fixture for a mock RestApiTool."""
mock_tool = mock.MagicMock(spec=RestApiTool)
mock_tool.name = "test_tool"
mock_tool.description = "Test Tool Description"
return mock_tool
@pytest.fixture
def mock_google_api_tool_instance(
mock_rest_api_tool,
): # Renamed from mock_google_api_tool
"""Fixture for a mock GoogleApiTool instance."""
mock_tool = mock.MagicMock(spec=GoogleApiTool)
mock_tool.name = "test_tool"
mock_tool.description = "Test Tool Description"
mock_tool.rest_api_tool = mock_rest_api_tool
return mock_tool
@pytest.fixture
def mock_rest_api_tools():
"""Fixture for a list of mock RestApiTools."""
tools = []
for i in range(3):
mock_tool = mock.MagicMock(
spec=RestApiTool, description=f"Test Tool Description {i}"
)
mock_tool.name = f"test_tool_{i}"
tools.append(mock_tool)
return tools
@pytest.fixture
def mock_openapi_toolset_instance(): # Renamed from mock_openapi_toolset
"""Fixture for a mock OpenAPIToolset instance."""
mock_toolset = mock.MagicMock(spec=OpenAPIToolset)
# Mock async methods if they are called
mock_toolset.get_tools = mock.AsyncMock(return_value=[])
mock_toolset.close = mock.AsyncMock()
return mock_toolset
@pytest.fixture
def mock_converter_instance(): # Renamed from mock_converter
"""Fixture for a mock GoogleApiToOpenApiConverter instance."""
mock_conv = mock.MagicMock(spec=GoogleApiToOpenApiConverter)
mock_conv.convert.return_value = {
"components": {
"securitySchemes": {
"oauth2": {
"flows": {
"authorizationCode": {
"scopes": {
DEFAULT_SCOPE: "Full access to Google Calendar"
}
}
}
}
}
}
}
return mock_conv
@pytest.fixture
def mock_readonly_context():
"""Fixture for a mock ReadonlyContext."""
return mock.MagicMock(spec=ReadonlyContext)
class TestGoogleApiToolset:
"""Test suite for the GoogleApiToolset class."""
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_init(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test GoogleApiToolset initialization."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
client_id = "test_client_id"
client_secret = "test_client_secret"
additional_headers = {"developer-token": "abc123"}
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
client_id=client_id,
client_secret=client_secret,
additional_headers=additional_headers,
)
assert tool_set.api_name == TEST_API_NAME
assert tool_set.api_version == TEST_API_VERSION
assert tool_set._client_id == client_id
assert tool_set._client_secret == client_secret
assert tool_set._service_account is None
assert tool_set.tool_filter is None
assert tool_set._openapi_toolset == mock_openapi_toolset_instance
assert tool_set._additional_headers == additional_headers
mock_converter_class.assert_called_once_with(
TEST_API_NAME, TEST_API_VERSION, discovery_url=None
)
mock_converter_instance.convert.assert_called_once()
spec_dict = mock_converter_instance.convert.return_value
mock_openapi_toolset_class.assert_called_once()
_, kwargs = mock_openapi_toolset_class.call_args
assert kwargs["spec_dict"] == spec_dict
assert kwargs["spec_str_type"] == "yaml"
assert isinstance(kwargs["auth_scheme"], OpenIdConnectWithConfig)
assert kwargs["auth_scheme"].scopes == [DEFAULT_SCOPE]
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_init_with_additional_scopes(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test GoogleApiToolset initialization with additional scopes."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
extra_scopes = [
DEFAULT_SCOPE,
"https://www.googleapis.com/auth/calendar.readonly",
]
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
additional_scopes=extra_scopes,
)
mock_openapi_toolset_class.assert_called_once()
_, kwargs = mock_openapi_toolset_class.call_args
assert isinstance(kwargs["auth_scheme"], OpenIdConnectWithConfig)
assert kwargs["auth_scheme"].scopes == [
DEFAULT_SCOPE,
"https://www.googleapis.com/auth/calendar.readonly",
]
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_init_with_discovery_url(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test GoogleApiToolset initialization with custom discovery URL."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
discovery_url = "https://example.com/discovery"
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
discovery_url=discovery_url,
)
mock_converter_class.assert_called_once_with(
TEST_API_NAME, TEST_API_VERSION, discovery_url=discovery_url
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiTool"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
async def test_get_tools(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_google_api_tool_class,
mock_converter_instance,
mock_openapi_toolset_instance,
mock_rest_api_tools,
mock_readonly_context,
):
"""Test get_tools method."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
mock_openapi_toolset_instance.get_tools = mock.AsyncMock(
return_value=mock_rest_api_tools
)
# Setup mock GoogleApiTool instances to be returned by the constructor
mock_google_api_tool_instances = [
mock.MagicMock(spec=GoogleApiTool, name=f"google_tool_{i}")
for i in range(len(mock_rest_api_tools))
]
mock_google_api_tool_class.side_effect = mock_google_api_tool_instances
client_id = "cid"
client_secret = "csecret"
sa_mock = mock.MagicMock(spec=ServiceAccount)
additional_headers = {"developer-token": "token"}
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
client_id=client_id,
client_secret=client_secret,
service_account=sa_mock,
additional_headers=additional_headers,
)
tools = await tool_set.get_tools(mock_readonly_context)
assert len(tools) == len(mock_rest_api_tools)
mock_openapi_toolset_instance.get_tools.assert_called_once_with(
mock_readonly_context
)
for i, rest_tool in enumerate(mock_rest_api_tools):
mock_google_api_tool_class.assert_any_call(
rest_tool,
client_id,
client_secret,
sa_mock,
additional_headers=additional_headers,
)
assert tools[i] is mock_google_api_tool_instances[i]
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
async def test_get_tools_with_filter_list(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_openapi_toolset_instance,
mock_rest_api_tools, # Has test_tool_0, test_tool_1, test_tool_2
mock_readonly_context,
mock_converter_instance,
):
"""Test get_tools method with a list filter."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
mock_openapi_toolset_instance.get_tools = mock.AsyncMock(
return_value=mock_rest_api_tools
)
tool_filter = ["test_tool_0", "test_tool_2"]
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
tool_filter=tool_filter,
)
tools = await tool_set.get_tools(mock_readonly_context)
assert len(tools) == 2
assert tools[0].name == "test_tool_0"
assert tools[1].name == "test_tool_2"
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
async def test_get_tools_with_filter_predicate(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
mock_rest_api_tools, # Has test_tool_0, test_tool_1, test_tool_2
mock_readonly_context,
):
"""Test get_tools method with a predicate filter."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
mock_openapi_toolset_instance.get_tools = mock.AsyncMock(
return_value=mock_rest_api_tools
)
class MyPredicate(ToolPredicate):
def __call__(
self,
tool: BaseTool,
readonly_context: Optional[ReadonlyContext] = None,
) -> bool:
return tool.name == "test_tool_1"
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
tool_filter=MyPredicate(),
)
tools = await tool_set.get_tools(mock_readonly_context)
assert len(tools) == 1
assert tools[0].name == "test_tool_1"
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_configure_auth(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test configure_auth method."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
client_id = "test_client_id"
client_secret = "test_client_secret"
tool_set.configure_auth(client_id, client_secret)
assert tool_set._client_id == client_id
assert tool_set._client_secret == client_secret
# To verify its effect, we would ideally call get_tools and check
# how GoogleApiTool is instantiated. This is covered in test_get_tools.
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_configure_sa_auth(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test configure_sa_auth method."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
service_account = ServiceAccount(
service_account_credential=ServiceAccountCredential(
type="service_account",
project_id="project_id",
private_key_id="private_key_id",
private_key=(
"-----BEGIN PRIVATE KEY-----\nprivate_key\n-----END PRIVATE"
" KEY-----\n"
),
client_email="client_email",
client_id="client_id",
auth_uri="auth_uri",
token_uri="token_uri",
auth_provider_x509_cert_url="auth_provider_x509_cert_url",
client_x509_cert_url="client_x509_cert_url",
universe_domain="universe_domain",
),
scopes=["scope1", "scope2"],
)
tool_set.configure_sa_auth(service_account)
assert tool_set._service_account == service_account
# Effect verification is covered in test_get_tools.
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
async def test_close(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test close method."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
await tool_set.close()
mock_openapi_toolset_instance.close.assert_called_once()
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_set_tool_filter(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test set_tool_filter method."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
assert tool_set.tool_filter is None
new_filter_list = ["tool1", "tool2"]
tool_set.set_tool_filter(new_filter_list)
assert tool_set.tool_filter == new_filter_list
def new_filter_predicate(
tool_name: str,
tool: RestApiTool,
readonly_context: Optional[ReadonlyContext] = None,
) -> bool:
return True
tool_set.set_tool_filter(new_filter_predicate)
assert tool_set.tool_filter == new_filter_predicate
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
def test_init_with_tool_name_prefix(
self,
mock_converter_class,
mock_openapi_toolset_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test GoogleApiToolset initialization with tool_name_prefix."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
tool_name_prefix = "test_prefix"
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME,
api_version=TEST_API_VERSION,
tool_name_prefix=tool_name_prefix,
)
assert tool_set.tool_name_prefix == tool_name_prefix
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.MtlsClientCerts"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.use_client_cert_effective"
)
async def test_mtls_cleanup_on_close(
self,
mock_use_client_cert,
mock_mtls_certs_class,
mock_converter_class,
mock_openapi_toolset_class,
):
"""Test that mTLS temp files are cleaned up on close."""
mock_converter_class.return_value = mock.MagicMock()
mock_openapi_toolset_instance = mock.MagicMock()
mock_openapi_toolset_instance.close = mock.AsyncMock()
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
mock_use_client_cert.return_value = True
mock_mtls_certs_instance = mock.MagicMock()
mock_mtls_certs_instance.get_certs.return_value = ("cert", "key", b"pass")
mock_mtls_certs_class.return_value = mock_mtls_certs_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
assert tool_set._httpx_client_factory is not None
await tool_set.close()
mock_openapi_toolset_instance.close.assert_called_once()
mock_mtls_certs_instance.close.assert_called_once()
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.httpx.AsyncClient"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.OpenAPIToolset"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.GoogleApiToOpenApiConverter"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.MtlsClientCerts"
)
@mock.patch(
"google.adk.tools.google_api_tool.google_api_toolset.use_client_cert_effective"
)
async def test_mtls_no_passphrase(
self,
mock_use_client_cert,
mock_mtls_certs_class,
mock_converter_class,
mock_openapi_toolset_class,
mock_async_client_class,
mock_converter_instance,
mock_openapi_toolset_instance,
):
"""Test that mTLS is configured even if key passphrase is None."""
mock_converter_class.return_value = mock_converter_instance
mock_openapi_toolset_class.return_value = mock_openapi_toolset_instance
mock_use_client_cert.return_value = True
mock_mtls_certs_instance = mock.MagicMock()
mock_mtls_certs_instance.get_certs.return_value = ("cert", "key", None)
mock_mtls_certs_class.return_value = mock_mtls_certs_instance
tool_set = GoogleApiToolset(
api_name=TEST_API_NAME, api_version=TEST_API_VERSION
)
assert tool_set._httpx_client_factory is not None
client = tool_set._httpx_client_factory()
assert client is not None
mock_async_client_class.assert_called_once_with(cert=("cert", "key"))
@@ -0,0 +1,773 @@
# 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.
from unittest.mock import MagicMock
from google.adk.tools.google_api_tool.googleapi_to_openapi_converter import GoogleApiToOpenApiConverter
# Import the converter class
from googleapiclient.errors import HttpError
import pytest
@pytest.fixture
def calendar_api_spec():
"""Fixture that provides a mock Google Calendar API spec for testing."""
return {
"kind": "discovery#restDescription",
"id": "calendar:v3",
"name": "calendar",
"version": "v3",
"title": "Google Calendar API",
"description": "Accesses the Google Calendar API",
"documentationLink": "https://developers.google.com/calendar/",
"protocol": "rest",
"rootUrl": "https://www.googleapis.com/",
"servicePath": "calendar/v3/",
"auth": {
"oauth2": {
"scopes": {
"https://www.googleapis.com/auth/calendar": {
"description": "Full access to Google Calendar"
},
"https://www.googleapis.com/auth/calendar.readonly": {
"description": "Read-only access to Google Calendar"
},
}
}
},
"schemas": {
"Calendar": {
"type": "object",
"description": "A calendar resource",
"properties": {
"id": {
"type": "string",
"description": "Calendar identifier",
},
"summary": {
"type": "string",
"description": "Calendar summary",
"required": True,
},
"timeZone": {
"type": "string",
"description": "Calendar timezone",
},
},
},
"Event": {
"type": "object",
"description": "An event resource",
"properties": {
"id": {"type": "string", "description": "Event identifier"},
"summary": {"type": "string", "description": "Event summary"},
"start": {"$ref": "EventDateTime"},
"end": {"$ref": "EventDateTime"},
"attendees": {
"type": "array",
"description": "Event attendees",
"items": {"$ref": "EventAttendee"},
},
},
},
"EventDateTime": {
"type": "object",
"description": "Date/time for an event",
"properties": {
"dateTime": {
"type": "string",
"format": "date-time",
"description": "Date/time in RFC3339 format",
},
"timeZone": {
"type": "string",
"description": "Timezone for the date/time",
},
},
},
"EventAttendee": {
"type": "object",
"description": "An attendee of an event",
"properties": {
"email": {"type": "string", "description": "Attendee email"},
"responseStatus": {
"type": "string",
"description": "Response status",
"enum": [
"needsAction",
"declined",
"tentative",
"accepted",
],
},
},
},
},
"resources": {
"calendars": {
"methods": {
"get": {
"id": "calendar.calendars.get",
"flatPath": "calendars/{calendarId}",
"httpMethod": "GET",
"description": "Returns metadata for a calendar.",
"parameters": {
"calendarId": {
"type": "string",
"description": "Calendar identifier",
"required": True,
"location": "path",
}
},
"response": {"$ref": "Calendar"},
"scopes": [
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.readonly",
],
},
"insert": {
"id": "calendar.calendars.insert",
"path": "calendars",
"httpMethod": "POST",
"description": "Creates a secondary calendar.",
"request": {"$ref": "Calendar"},
"response": {"$ref": "Calendar"},
"scopes": ["https://www.googleapis.com/auth/calendar"],
},
},
"resources": {
"events": {
"methods": {
"list": {
"id": "calendar.events.list",
"flatPath": "calendars/{calendarId}/events",
"httpMethod": "GET",
"description": (
"Returns events on the specified calendar."
),
"parameters": {
"calendarId": {
"type": "string",
"description": "Calendar identifier",
"required": True,
"location": "path",
},
"maxResults": {
"type": "integer",
"description": (
"Maximum number of events returned"
),
"format": "int32",
"minimum": "1",
"maximum": "2500",
"default": "250",
"location": "query",
},
"orderBy": {
"type": "string",
"description": (
"Order of the events returned"
),
"enum": ["startTime", "updated"],
"location": "query",
},
},
"response": {"$ref": "Events"},
"scopes": [
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.readonly",
],
}
}
}
},
}
},
}
@pytest.fixture(autouse=True)
def disable_mtls_by_default(monkeypatch):
monkeypatch.setattr(
"google.auth.transport.mtls.should_use_client_cert",
lambda: False,
)
@pytest.fixture
def converter():
"""Fixture that provides a basic converter instance."""
return GoogleApiToOpenApiConverter("calendar", "v3")
@pytest.fixture
def mock_api_resource(calendar_api_spec):
"""Fixture that provides a mock API resource with the test spec."""
mock_resource = MagicMock()
mock_resource._rootDesc = calendar_api_spec
return mock_resource
@pytest.fixture
def prepared_converter(converter, calendar_api_spec):
"""Fixture that provides a converter with the API spec already set."""
converter._google_api_spec = calendar_api_spec
return converter
@pytest.fixture
def converter_with_patched_build(monkeypatch, mock_api_resource):
"""Fixture that provides a converter with the build function patched.
This simulates a successful API spec fetch.
"""
# Create a mock for the build function
mock_build = MagicMock(return_value=mock_api_resource)
# Patch the build function in the target module
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
# Create and return a converter instance
return GoogleApiToOpenApiConverter("calendar", "v3")
class TestGoogleApiToOpenApiConverter:
"""Test suite for the GoogleApiToOpenApiConverter class."""
def test_init(self, converter):
"""Test converter initialization."""
assert converter._api_name == "calendar"
assert converter._api_version == "v3"
assert converter._google_api_resource is None
assert converter._google_api_spec is None
assert converter._openapi_spec["openapi"] == "3.0.0"
assert "info" in converter._openapi_spec
assert "paths" in converter._openapi_spec
assert "components" in converter._openapi_spec
def test_fetch_google_api_spec(
self, converter_with_patched_build, calendar_api_spec
):
"""Test fetching Google API specification."""
# Call the method
converter_with_patched_build.fetch_google_api_spec()
# Verify the results
assert converter_with_patched_build._google_api_spec == calendar_api_spec
def test_fetch_google_api_spec_with_discovery_url(
self, monkeypatch, mock_api_resource, calendar_api_spec
):
"""Test fetching Google API specification with custom discovery URL."""
mock_build = MagicMock(return_value=mock_api_resource)
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
discovery_url = "https://example.com/discovery"
converter = GoogleApiToOpenApiConverter(
"calendar", "v3", discovery_url=discovery_url
)
converter.fetch_google_api_spec()
assert converter._google_api_spec == calendar_api_spec
mock_build.assert_called_once_with(
"calendar", "v3", discoveryServiceUrl=discovery_url, http=None
)
def test_fetch_google_api_spec_with_mtls(
self, monkeypatch, mock_api_resource, calendar_api_spec
):
"""Test fetching Google API specification with mTLS enabled."""
mock_build = MagicMock(return_value=mock_api_resource)
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
# Enable mTLS
monkeypatch.setattr(
"google.auth.transport.mtls.should_use_client_cert",
lambda: True,
)
monkeypatch.setattr(
"google.auth.transport.mtls.has_default_client_cert_source",
lambda: True,
)
mock_cert_source = MagicMock(
return_value=("/path/to/cert", "/path/to/key", b"passphrase")
)
monkeypatch.setattr(
"google.auth.transport.mtls.default_client_encrypted_cert_source",
lambda c, k: mock_cert_source,
)
converter = GoogleApiToOpenApiConverter("calendar", "v3")
converter.fetch_google_api_spec()
assert converter._google_api_spec == calendar_api_spec
# Verify build was called with the http parameter set and mtls url
mock_build.assert_called_once()
_, kwargs = mock_build.call_args
assert "http" in kwargs
assert kwargs["http"] is not None
assert (
kwargs["discoveryServiceUrl"]
== "https://www.mtls.googleapis.com/discovery/v1/apis/{api}/{apiVersion}/rest"
)
def test_fetch_google_api_spec_with_mtls_no_passphrase(
self, monkeypatch, mock_api_resource, calendar_api_spec
):
"""Test fetching Google API specification with mTLS enabled but no passphrase."""
mock_build = MagicMock(return_value=mock_api_resource)
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
# Enable mTLS
monkeypatch.setattr(
"google.auth.transport.mtls.should_use_client_cert",
lambda: True,
)
monkeypatch.setattr(
"google.auth.transport.mtls.has_default_client_cert_source",
lambda: True,
)
# Return None for passphrase
mock_cert_source = MagicMock(
return_value=("/path/to/cert", "/path/to/key", None)
)
monkeypatch.setattr(
"google.auth.transport.mtls.default_client_encrypted_cert_source",
lambda c, k: mock_cert_source,
)
converter = GoogleApiToOpenApiConverter("calendar", "v3")
converter.fetch_google_api_spec()
assert converter._google_api_spec == calendar_api_spec
# Verify build was called with the http parameter set and mtls url
mock_build.assert_called_once()
_, kwargs = mock_build.call_args
assert "http" in kwargs
assert kwargs["http"] is not None
assert (
kwargs["discoveryServiceUrl"]
== "https://www.mtls.googleapis.com/discovery/v1/apis/{api}/{apiVersion}/rest"
)
def test_fetch_google_api_spec_error(self, monkeypatch, converter):
"""Test error handling when fetching Google API specification."""
# Create a mock that raises an error
mock_build = MagicMock(
side_effect=HttpError(resp=MagicMock(status=404), content=b"Not Found")
)
monkeypatch.setattr(
"google.adk.tools.google_api_tool.googleapi_to_openapi_converter.build",
mock_build,
)
# Verify exception is raised
with pytest.raises(HttpError):
converter.fetch_google_api_spec()
def test_convert_info(self, prepared_converter):
"""Test conversion of basic API information."""
# Call the method
prepared_converter._convert_info()
# Verify the results
info = prepared_converter._openapi_spec["info"]
assert info["title"] == "Google Calendar API"
assert info["description"] == "Accesses the Google Calendar API"
assert info["version"] == "v3"
assert info["termsOfService"] == "https://developers.google.com/calendar/"
# Check external docs
external_docs = prepared_converter._openapi_spec["externalDocs"]
assert external_docs["url"] == "https://developers.google.com/calendar/"
def test_convert_servers(self, prepared_converter):
"""Test conversion of server information."""
# Call the method
prepared_converter._convert_servers()
# Verify the results
servers = prepared_converter._openapi_spec["servers"]
assert len(servers) == 1
assert servers[0]["url"] == "https://www.googleapis.com/calendar/v3"
assert servers[0]["description"] == "calendar v3 API"
def test_convert_security_schemes(self, prepared_converter):
"""Test conversion of security schemes."""
# Call the method
prepared_converter._convert_security_schemes()
# Verify the results
security_schemes = prepared_converter._openapi_spec["components"][
"securitySchemes"
]
# Check OAuth2 configuration
assert "oauth2" in security_schemes
oauth2 = security_schemes["oauth2"]
assert oauth2["type"] == "oauth2"
# Check OAuth2 scopes
scopes = oauth2["flows"]["authorizationCode"]["scopes"]
assert "https://www.googleapis.com/auth/calendar" in scopes
assert "https://www.googleapis.com/auth/calendar.readonly" in scopes
# Check API key configuration
assert "apiKey" in security_schemes
assert security_schemes["apiKey"]["type"] == "apiKey"
assert security_schemes["apiKey"]["in"] == "query"
assert security_schemes["apiKey"]["name"] == "key"
def test_convert_schemas(self, prepared_converter):
"""Test conversion of schema definitions."""
# Call the method
prepared_converter._convert_schemas()
# Verify the results
schemas = prepared_converter._openapi_spec["components"]["schemas"]
# Check Calendar schema
assert "Calendar" in schemas
calendar_schema = schemas["Calendar"]
assert calendar_schema["type"] == "object"
assert calendar_schema["description"] == "A calendar resource"
# Check required properties
assert "required" in calendar_schema
assert "summary" in calendar_schema["required"]
# Check Event schema references
assert "Event" in schemas
event_schema = schemas["Event"]
assert (
event_schema["properties"]["start"]["$ref"]
== "#/components/schemas/EventDateTime"
)
# Check array type with references
attendees_schema = event_schema["properties"]["attendees"]
assert attendees_schema["type"] == "array"
assert (
attendees_schema["items"]["$ref"]
== "#/components/schemas/EventAttendee"
)
# Check enum values
attendee_schema = schemas["EventAttendee"]
response_status = attendee_schema["properties"]["responseStatus"]
assert "enum" in response_status
assert "accepted" in response_status["enum"]
@pytest.mark.parametrize(
"schema_def, expected_type, expected_attrs",
[
# Test object type
(
{
"type": "object",
"description": "Test object",
"properties": {
"id": {"type": "string", "required": True},
"name": {"type": "string"},
},
},
"object",
{"description": "Test object", "required": ["id"]},
),
# Test array type
(
{
"type": "array",
"description": "Test array",
"items": {"type": "string"},
},
"array",
{"description": "Test array", "items": {"type": "string"}},
),
# Test reference conversion
(
{"$ref": "Calendar"},
None, # No type for references
{"$ref": "#/components/schemas/Calendar"},
),
# Test enum conversion
(
{"type": "string", "enum": ["value1", "value2"]},
"string",
{"enum": ["value1", "value2"]},
),
],
)
def test_convert_schema_object(
self, converter, schema_def, expected_type, expected_attrs
):
"""Test conversion of individual schema objects with different input variations."""
converted = converter._convert_schema_object(schema_def)
# Check type if expected
if expected_type:
assert converted["type"] == expected_type
# Check other expected attributes
for key, value in expected_attrs.items():
assert converted[key] == value
@pytest.mark.parametrize(
"path, expected_params",
[
# Path with parameters
(
"/calendars/{calendarId}/events/{eventId}",
["calendarId", "eventId"],
),
# Path without parameters
("/calendars/events", []),
# Mixed path
("/users/{userId}/calendars/default", ["userId"]),
],
)
def test_extract_path_parameters(self, converter, path, expected_params):
"""Test extraction of path parameters from URL path with various inputs."""
params = converter._extract_path_parameters(path)
assert set(params) == set(expected_params)
assert len(params) == len(expected_params)
@pytest.mark.parametrize(
"param_data, expected_result",
[
# String parameter
(
{
"type": "string",
"description": "String parameter",
"pattern": "^[a-z]+$",
},
{"type": "string", "pattern": "^[a-z]+$"},
),
# Integer parameter with format
(
{"type": "integer", "format": "int32", "default": "10"},
{"type": "integer", "format": "int32", "default": "10"},
),
# Enum parameter
(
{"type": "string", "enum": ["option1", "option2"]},
{"type": "string", "enum": ["option1", "option2"]},
),
],
)
def test_convert_parameter_schema(
self, converter, param_data, expected_result
):
"""Test conversion of parameter definitions to OpenAPI schemas."""
converted = converter._convert_parameter_schema(param_data)
# Check all expected attributes
for key, value in expected_result.items():
assert converted[key] == value
def test_convert(self, converter_with_patched_build):
"""Test the complete conversion process."""
# Call the method
result = converter_with_patched_build.convert()
# Verify basic structure
assert result["openapi"] == "3.0.0"
assert "info" in result
assert "servers" in result
assert "paths" in result
assert "components" in result
# Verify paths
paths = result["paths"]
assert "/calendars/{calendarId}" in paths
assert "get" in paths["/calendars/{calendarId}"]
# Verify nested resources
assert "/calendars/{calendarId}/events" in paths
# Verify method details
get_calendar = paths["/calendars/{calendarId}"]["get"]
assert get_calendar["operationId"] == "calendar.calendars.get"
assert "parameters" in get_calendar
# Verify request body
insert_calendar = paths["/calendars"]["post"]
assert "requestBody" in insert_calendar
request_schema = insert_calendar["requestBody"]["content"][
"application/json"
]["schema"]
assert request_schema["$ref"] == "#/components/schemas/Calendar"
# Verify response body
assert "responses" in get_calendar
response_schema = get_calendar["responses"]["200"]["content"][
"application/json"
]["schema"]
assert response_schema["$ref"] == "#/components/schemas/Calendar"
def test_convert_methods(self, prepared_converter, calendar_api_spec):
"""Test conversion of API methods."""
# Convert methods
methods = calendar_api_spec["resources"]["calendars"]["methods"]
prepared_converter._convert_methods(methods, "/calendars")
# Verify the results
paths = prepared_converter._openapi_spec["paths"]
# Check GET method
assert "/calendars/{calendarId}" in paths
get_method = paths["/calendars/{calendarId}"]["get"]
assert get_method["operationId"] == "calendar.calendars.get"
# Check parameters
params = get_method["parameters"]
param_names = [p["name"] for p in params]
assert "calendarId" in param_names
# Check POST method
assert "/calendars" in paths
post_method = paths["/calendars"]["post"]
assert post_method["operationId"] == "calendar.calendars.insert"
# Check request body
assert "requestBody" in post_method
assert (
post_method["requestBody"]["content"]["application/json"]["schema"][
"$ref"
]
== "#/components/schemas/Calendar"
)
# Check response
assert (
post_method["responses"]["200"]["content"]["application/json"][
"schema"
]["$ref"]
== "#/components/schemas/Calendar"
)
def test_convert_resources(self, prepared_converter, calendar_api_spec):
"""Test conversion of nested resources."""
# Convert resources
resources = calendar_api_spec["resources"]
prepared_converter._convert_resources(resources)
# Verify the results
paths = prepared_converter._openapi_spec["paths"]
# Check top-level resource methods
assert "/calendars/{calendarId}" in paths
# Check nested resource methods
assert "/calendars/{calendarId}/events" in paths
events_method = paths["/calendars/{calendarId}/events"]["get"]
assert events_method["operationId"] == "calendar.events.list"
# Check parameters in nested resource
params = events_method["parameters"]
param_names = [p["name"] for p in params]
assert "calendarId" in param_names
assert "maxResults" in param_names
assert "orderBy" in param_names
def test_integration_calendar_api(self, converter_with_patched_build):
"""Integration test using Calendar API specification."""
# Create and run the converter
openapi_spec = converter_with_patched_build.convert()
# Verify conversion results
assert openapi_spec["info"]["title"] == "Google Calendar API"
assert (
openapi_spec["servers"][0]["url"]
== "https://www.googleapis.com/calendar/v3"
)
# Check security schemes
security_schemes = openapi_spec["components"]["securitySchemes"]
assert "oauth2" in security_schemes
assert "apiKey" in security_schemes
# Check schemas
schemas = openapi_spec["components"]["schemas"]
assert "Calendar" in schemas
assert "Event" in schemas
assert "EventDateTime" in schemas
# Check paths
paths = openapi_spec["paths"]
assert "/calendars/{calendarId}" in paths
assert "/calendars" in paths
assert "/calendars/{calendarId}/events" in paths
# Check method details
get_events = paths["/calendars/{calendarId}/events"]["get"]
assert get_events["operationId"] == "calendar.events.list"
# Check parameter details
param_dict = {p["name"]: p for p in get_events["parameters"]}
assert "maxResults" in param_dict
max_results = param_dict["maxResults"]
assert max_results["in"] == "query"
assert max_results["schema"]["type"] == "integer"
assert max_results["schema"]["default"] == "250"
@pytest.fixture
def conftest_content():
"""Returns content for a conftest.py file to help with testing."""
return """
import pytest
from unittest.mock import MagicMock
# This file contains fixtures that can be shared across multiple test modules
@pytest.fixture
def mock_google_response():
\"\"\"Fixture that provides a mock response from Google's API.\"\"\"
return {"key": "value", "items": [{"id": 1}, {"id": 2}]}
@pytest.fixture
def mock_http_error():
\"\"\"Fixture that provides a mock HTTP error.\"\"\"
mock_resp = MagicMock()
mock_resp.status = 404
return HttpError(resp=mock_resp, content=b'Not Found')
"""
def test_generate_conftest_example(conftest_content):
"""This is a meta-test that demonstrates how to generate a conftest.py file.
In a real project, you would create a separate conftest.py file.
"""
# In a real scenario, you would write this to a file named conftest.py
# This test just verifies the conftest content is not empty
assert len(conftest_content) > 0