Add comprehensive tests and documentation for Apps Script

Testing:
- Created unit tests with mocked API responses (test_apps_script_tools.py)
- 20+ test cases covering all 11 tools
- Tests for success cases, edge cases, and error handling
- Created manual test script for real API testing (manual_test.py)

Documentation:
- Created gappsscript module README with usage examples
- Updated main README.md with Apps Script section
- Added Apps Script to features list
- Added Apps Script API to enable links
- Added complete tool reference table
- No em dashes or emojis per requirements

All documentation follows existing codebase style and patterns.
This commit is contained in:
sam-ent
2026-01-13 19:29:44 +00:00
parent f5702b32b8
commit fb951d6314
6 changed files with 1244 additions and 0 deletions

View File

View File

@@ -0,0 +1,317 @@
"""
Manual test script for Apps Script integration
This script allows manual testing of Apps Script tools against real Google API.
Requires valid OAuth credentials and enabled Apps Script API.
Usage:
python tests/gappsscript/manual_test.py
Note: This will interact with real Apps Script projects.
Use with caution and in a test environment.
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import pickle
SCOPES = [
"https://www.googleapis.com/auth/script.projects",
"https://www.googleapis.com/auth/script.deployments",
]
def get_credentials():
"""
Get OAuth credentials for Apps Script API.
Returns:
Credentials object
"""
creds = None
token_path = "token.pickle"
if os.path.exists(token_path):
with open(token_path, "rb") as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
if not os.path.exists("client_secret.json"):
print("Error: client_secret.json not found")
print(
"Please download OAuth credentials from Google Cloud Console"
)
print(
"and save as client_secret.json in the project root"
)
sys.exit(1)
flow = InstalledAppFlow.from_client_secrets_file(
"client_secret.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open(token_path, "wb") as token:
pickle.dump(creds, token)
return creds
async def test_list_projects(service):
"""Test listing Apps Script projects"""
print("\n=== Test: List Projects ===")
from gappsscript.apps_script_tools import list_script_projects
try:
result = await list_script_projects(
service=service, user_google_email="test@example.com", page_size=10
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def test_create_project(service):
"""Test creating a new Apps Script project"""
print("\n=== Test: Create Project ===")
from gappsscript.apps_script_tools import create_script_project
try:
result = await create_script_project(
service=service,
user_google_email="test@example.com",
title="MCP Test Project",
)
print(result)
if "Script ID:" in result:
script_id = result.split("Script ID: ")[1].split("\n")[0]
return script_id
return None
except Exception as e:
print(f"Error: {e}")
return None
async def test_get_project(service, script_id):
"""Test retrieving project details"""
print(f"\n=== Test: Get Project {script_id} ===")
from gappsscript.apps_script_tools import get_script_project
try:
result = await get_script_project(
service=service, user_google_email="test@example.com", script_id=script_id
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def test_update_content(service, script_id):
"""Test updating script content"""
print(f"\n=== Test: Update Content {script_id} ===")
from gappsscript.apps_script_tools import update_script_content
files = [
{
"name": "Code",
"type": "SERVER_JS",
"source": """function testFunction() {
Logger.log('Hello from MCP test!');
return 'Test successful';
}""",
}
]
try:
result = await update_script_content(
service=service,
user_google_email="test@example.com",
script_id=script_id,
files=files,
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def test_run_function(service, script_id):
"""Test running a script function"""
print(f"\n=== Test: Run Function {script_id} ===")
from gappsscript.apps_script_tools import run_script_function
try:
result = await run_script_function(
service=service,
user_google_email="test@example.com",
script_id=script_id,
function_name="testFunction",
dev_mode=True,
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def test_create_deployment(service, script_id):
"""Test creating a deployment"""
print(f"\n=== Test: Create Deployment {script_id} ===")
from gappsscript.apps_script_tools import create_deployment
try:
result = await create_deployment(
service=service,
user_google_email="test@example.com",
script_id=script_id,
description="MCP Test Deployment",
)
print(result)
if "Deployment ID:" in result:
deployment_id = result.split("Deployment ID: ")[1].split("\n")[0]
return deployment_id
return None
except Exception as e:
print(f"Error: {e}")
return None
async def test_list_deployments(service, script_id):
"""Test listing deployments"""
print(f"\n=== Test: List Deployments {script_id} ===")
from gappsscript.apps_script_tools import list_deployments
try:
result = await list_deployments(
service=service, user_google_email="test@example.com", script_id=script_id
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def test_list_processes(service):
"""Test listing script processes"""
print("\n=== Test: List Processes ===")
from gappsscript.apps_script_tools import list_script_processes
try:
result = await list_script_processes(
service=service, user_google_email="test@example.com", page_size=10
)
print(result)
return True
except Exception as e:
print(f"Error: {e}")
return False
async def cleanup_test_project(service, script_id):
"""
Cleanup test project (requires Drive API).
Note: Apps Script API does not have a delete endpoint.
Projects must be deleted via Drive API by moving to trash.
"""
print(f"\n=== Cleanup: Delete Project {script_id} ===")
print("Note: Apps Script projects must be deleted via Drive API")
print(f"Please manually delete: https://script.google.com/d/{script_id}/edit")
async def run_all_tests():
"""Run all manual tests"""
print("="*60)
print("Apps Script MCP Manual Test Suite")
print("="*60)
print("\nGetting OAuth credentials...")
creds = get_credentials()
print("Building Apps Script API service...")
service = build("script", "v1", credentials=creds)
test_script_id = None
deployment_id = None
try:
success = await test_list_projects(service)
if not success:
print("\nWarning: List projects failed")
test_script_id = await test_create_project(service)
if test_script_id:
print(f"\nCreated test project: {test_script_id}")
await test_get_project(service, test_script_id)
await test_update_content(service, test_script_id)
await asyncio.sleep(2)
await test_run_function(service, test_script_id)
deployment_id = await test_create_deployment(service, test_script_id)
if deployment_id:
print(f"\nCreated deployment: {deployment_id}")
await test_list_deployments(service, test_script_id)
else:
print("\nSkipping tests that require a project (creation failed)")
await test_list_processes(service)
finally:
if test_script_id:
await cleanup_test_project(service, test_script_id)
print("\n" + "="*60)
print("Manual Test Suite Complete")
print("="*60)
def main():
"""Main entry point"""
print("\nIMPORTANT: This script will:")
print("1. Create a test Apps Script project in your account")
print("2. Run various operations on it")
print("3. Leave the project for manual cleanup")
print("\nYou must manually delete the test project after running this.")
response = input("\nContinue? (yes/no): ")
if response.lower() not in ["yes", "y"]:
print("Aborted")
return
asyncio.run(run_all_tests())
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,576 @@
"""
Unit tests for Google Apps Script MCP tools
Tests all Apps Script tools with mocked API responses
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
import asyncio
@pytest.mark.asyncio
async def test_list_script_projects():
"""Test listing Apps Script projects"""
from gappsscript.apps_script_tools import list_script_projects
mock_service = Mock()
mock_response = {
"projects": [
{
"scriptId": "test123",
"title": "Test Project",
"createTime": "2025-01-10T10:00:00Z",
"updateTime": "2026-01-12T15:30:00Z",
},
{
"scriptId": "test456",
"title": "Another Project",
"createTime": "2025-06-15T12:00:00Z",
"updateTime": "2025-12-20T09:45:00Z",
},
]
}
mock_service.projects().list().execute.return_value = mock_response
result = await list_script_projects(
service=mock_service, user_google_email="test@example.com", page_size=50
)
assert "Found 2 Apps Script projects" in result
assert "Test Project" in result
assert "test123" in result
assert "Another Project" in result
assert "test456" in result
mock_service.projects().list.assert_called_once_with(pageSize=50)
@pytest.mark.asyncio
async def test_list_script_projects_empty():
"""Test listing projects when none exist"""
from gappsscript.apps_script_tools import list_script_projects
mock_service = Mock()
mock_service.projects().list().execute.return_value = {"projects": []}
result = await list_script_projects(
service=mock_service, user_google_email="test@example.com"
)
assert result == "No Apps Script projects found."
@pytest.mark.asyncio
async def test_list_script_projects_with_pagination():
"""Test listing projects with pagination token"""
from gappsscript.apps_script_tools import list_script_projects
mock_service = Mock()
mock_response = {
"projects": [{"scriptId": "test123", "title": "Test"}],
"nextPageToken": "token123",
}
mock_service.projects().list().execute.return_value = mock_response
result = await list_script_projects(
service=mock_service,
user_google_email="test@example.com",
page_token="prev_token",
)
assert "Next page token: token123" in result
mock_service.projects().list.assert_called_once_with(
pageSize=50, pageToken="prev_token"
)
@pytest.mark.asyncio
async def test_get_script_project():
"""Test retrieving complete project details"""
from gappsscript.apps_script_tools import get_script_project
mock_service = Mock()
mock_response = {
"scriptId": "test123",
"title": "Test Project",
"creator": {"email": "creator@example.com"},
"createTime": "2025-01-10T10:00:00Z",
"updateTime": "2026-01-12T15:30:00Z",
"files": [
{"name": "Code.gs", "type": "SERVER_JS", "source": "function test() {}"},
{
"name": "appsscript.json",
"type": "JSON",
"source": '{"timeZone": "America/New_York"}',
},
],
}
mock_service.projects().get().execute.return_value = mock_response
result = await get_script_project(
service=mock_service, user_google_email="test@example.com", script_id="test123"
)
assert "Project: Test Project (ID: test123)" in result
assert "Creator: creator@example.com" in result
assert "Code.gs" in result
assert "appsscript.json" in result
mock_service.projects().get.assert_called_once_with(scriptId="test123")
@pytest.mark.asyncio
async def test_get_script_content():
"""Test retrieving specific file content"""
from gappsscript.apps_script_tools import get_script_content
mock_service = Mock()
mock_response = {
"scriptId": "test123",
"files": [
{
"name": "Code.gs",
"type": "SERVER_JS",
"source": "function sendEmail() {\n // code here\n}",
},
{"name": "Other.gs", "type": "SERVER_JS", "source": "function other() {}"},
],
}
mock_service.projects().get().execute.return_value = mock_response
result = await get_script_content(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
file_name="Code.gs",
)
assert "File: Code.gs" in result
assert "function sendEmail()" in result
mock_service.projects().get.assert_called_once_with(scriptId="test123")
@pytest.mark.asyncio
async def test_get_script_content_file_not_found():
"""Test retrieving non-existent file"""
from gappsscript.apps_script_tools import get_script_content
mock_service = Mock()
mock_response = {
"scriptId": "test123",
"files": [{"name": "Code.gs", "type": "SERVER_JS", "source": ""}],
}
mock_service.projects().get().execute.return_value = mock_response
result = await get_script_content(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
file_name="NonExistent.gs",
)
assert "File 'NonExistent.gs' not found" in result
@pytest.mark.asyncio
async def test_create_script_project():
"""Test creating new Apps Script project"""
from gappsscript.apps_script_tools import create_script_project
mock_service = Mock()
mock_response = {
"scriptId": "new123",
"title": "New Project",
}
mock_service.projects().create().execute.return_value = mock_response
result = await create_script_project(
service=mock_service,
user_google_email="test@example.com",
title="New Project",
)
assert "Created Apps Script project: New Project" in result
assert "Script ID: new123" in result
assert "https://script.google.com/d/new123/edit" in result
mock_service.projects().create.assert_called_once_with(body={"title": "New Project"})
@pytest.mark.asyncio
async def test_create_script_project_with_parent():
"""Test creating project with parent folder"""
from gappsscript.apps_script_tools import create_script_project
mock_service = Mock()
mock_response = {"scriptId": "new123", "title": "New Project"}
mock_service.projects().create().execute.return_value = mock_response
result = await create_script_project(
service=mock_service,
user_google_email="test@example.com",
title="New Project",
parent_id="folder123",
)
assert "Script ID: new123" in result
mock_service.projects().create.assert_called_once_with(
body={"title": "New Project", "parentId": "folder123"}
)
@pytest.mark.asyncio
async def test_update_script_content():
"""Test updating script project files"""
from gappsscript.apps_script_tools import update_script_content
mock_service = Mock()
files_to_update = [
{"name": "Code.gs", "type": "SERVER_JS", "source": "function test() {}"},
{"name": "Helper.gs", "type": "SERVER_JS", "source": "function helper() {}"},
]
mock_response = {
"scriptId": "test123",
"files": files_to_update,
}
mock_service.projects().updateContent().execute.return_value = mock_response
result = await update_script_content(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
files=files_to_update,
)
assert "Updated script project: test123" in result
assert "Code.gs" in result
assert "Helper.gs" in result
mock_service.projects().updateContent.assert_called_once_with(
scriptId="test123", body={"files": files_to_update}
)
@pytest.mark.asyncio
async def test_run_script_function_success():
"""Test successful script function execution"""
from gappsscript.apps_script_tools import run_script_function
mock_service = Mock()
mock_response = {
"response": {"result": "Email sent successfully"},
}
mock_service.scripts().run().execute.return_value = mock_response
result = await run_script_function(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
function_name="sendEmail",
)
assert "Execution successful" in result
assert "Function: sendEmail" in result
assert "Email sent successfully" in result
mock_service.scripts().run.assert_called_once_with(
scriptId="test123", body={"function": "sendEmail", "devMode": False}
)
@pytest.mark.asyncio
async def test_run_script_function_with_parameters():
"""Test running function with parameters"""
from gappsscript.apps_script_tools import run_script_function
mock_service = Mock()
mock_response = {"response": {"result": "Success"}}
mock_service.scripts().run().execute.return_value = mock_response
result = await run_script_function(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
function_name="processData",
parameters=["param1", 42, True],
dev_mode=True,
)
assert "Execution successful" in result
mock_service.scripts().run.assert_called_once_with(
scriptId="test123",
body={
"function": "processData",
"devMode": True,
"parameters": ["param1", 42, True],
},
)
@pytest.mark.asyncio
async def test_run_script_function_error():
"""Test script execution error handling"""
from gappsscript.apps_script_tools import run_script_function
mock_service = Mock()
mock_response = {
"error": {"message": "ReferenceError: variable is not defined"},
}
mock_service.scripts().run().execute.return_value = mock_response
result = await run_script_function(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
function_name="brokenFunction",
)
assert "Execution failed" in result
assert "brokenFunction" in result
assert "ReferenceError" in result
@pytest.mark.asyncio
async def test_create_deployment():
"""Test creating deployment"""
from gappsscript.apps_script_tools import create_deployment
mock_service = Mock()
mock_response = {
"deploymentId": "deploy123",
"deploymentConfig": {},
}
mock_service.projects().deployments().create().execute.return_value = (
mock_response
)
result = await create_deployment(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
description="Production deployment",
)
assert "Created deployment for script: test123" in result
assert "Deployment ID: deploy123" in result
assert "Production deployment" in result
mock_service.projects().deployments().create.assert_called_once_with(
scriptId="test123", body={"description": "Production deployment"}
)
@pytest.mark.asyncio
async def test_create_deployment_with_version():
"""Test creating deployment with version description"""
from gappsscript.apps_script_tools import create_deployment
mock_service = Mock()
mock_response = {"deploymentId": "deploy123"}
mock_service.projects().deployments().create().execute.return_value = (
mock_response
)
result = await create_deployment(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
description="Production",
version_description="Version 1.0",
)
assert "Deployment ID: deploy123" in result
call_args = mock_service.projects().deployments().create.call_args
assert call_args[1]["body"]["versionNumber"]["description"] == "Version 1.0"
@pytest.mark.asyncio
async def test_list_deployments():
"""Test listing deployments"""
from gappsscript.apps_script_tools import list_deployments
mock_service = Mock()
mock_response = {
"deployments": [
{
"deploymentId": "deploy1",
"description": "Production",
"updateTime": "2026-01-10T12:00:00Z",
},
{
"deploymentId": "deploy2",
"description": "Staging",
"updateTime": "2026-01-08T10:00:00Z",
},
]
}
mock_service.projects().deployments().list().execute.return_value = mock_response
result = await list_deployments(
service=mock_service, user_google_email="test@example.com", script_id="test123"
)
assert "Deployments for script: test123" in result
assert "Production" in result
assert "deploy1" in result
assert "Staging" in result
mock_service.projects().deployments().list.assert_called_once_with(
scriptId="test123"
)
@pytest.mark.asyncio
async def test_list_deployments_empty():
"""Test listing deployments when none exist"""
from gappsscript.apps_script_tools import list_deployments
mock_service = Mock()
mock_service.projects().deployments().list().execute.return_value = {
"deployments": []
}
result = await list_deployments(
service=mock_service, user_google_email="test@example.com", script_id="test123"
)
assert "No deployments found" in result
@pytest.mark.asyncio
async def test_update_deployment():
"""Test updating deployment"""
from gappsscript.apps_script_tools import update_deployment
mock_service = Mock()
mock_response = {
"deploymentId": "deploy123",
"description": "Updated description",
}
mock_service.projects().deployments().update().execute.return_value = (
mock_response
)
result = await update_deployment(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
deployment_id="deploy123",
description="Updated description",
)
assert "Updated deployment: deploy123" in result
assert "Script: test123" in result
mock_service.projects().deployments().update.assert_called_once_with(
scriptId="test123",
deploymentId="deploy123",
body={"description": "Updated description"},
)
@pytest.mark.asyncio
async def test_delete_deployment():
"""Test deleting deployment"""
from gappsscript.apps_script_tools import delete_deployment
mock_service = Mock()
mock_service.projects().deployments().delete().execute.return_value = {}
result = await delete_deployment(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
deployment_id="deploy123",
)
assert "Deleted deployment: deploy123 from script: test123" in result
mock_service.projects().deployments().delete.assert_called_once_with(
scriptId="test123", deploymentId="deploy123"
)
@pytest.mark.asyncio
async def test_list_script_processes():
"""Test listing script execution processes"""
from gappsscript.apps_script_tools import list_script_processes
mock_service = Mock()
mock_response = {
"processes": [
{
"functionName": "sendEmail",
"processType": "EDITOR",
"processStatus": "COMPLETED",
"startTime": "2026-01-13T09:00:00Z",
"duration": "2.3s",
"userAccessLevel": "OWNER",
},
{
"functionName": "processData",
"processType": "SIMPLE_TRIGGER",
"processStatus": "FAILED",
"startTime": "2026-01-13T08:55:00Z",
"duration": "1.1s",
},
]
}
mock_service.processes().list().execute.return_value = mock_response
result = await list_script_processes(
service=mock_service, user_google_email="test@example.com"
)
assert "Recent script executions" in result
assert "sendEmail" in result
assert "COMPLETED" in result
assert "processData" in result
assert "FAILED" in result
mock_service.processes().list.assert_called_once_with(pageSize=50)
@pytest.mark.asyncio
async def test_list_script_processes_filtered():
"""Test listing processes filtered by script ID"""
from gappsscript.apps_script_tools import list_script_processes
mock_service = Mock()
mock_response = {"processes": []}
mock_service.processes().list().execute.return_value = mock_response
result = await list_script_processes(
service=mock_service,
user_google_email="test@example.com",
script_id="test123",
)
assert "No recent script executions found" in result
mock_service.processes().list.assert_called_once_with(
pageSize=50, scriptId="test123"
)