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
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:
@@ -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
|
||||
Reference in New Issue
Block a user