347 lines
10 KiB
Python
347 lines
10 KiB
Python
"""
|
|
Unit tests for Google Forms MCP tools
|
|
|
|
Tests the batch_update_form tool with mocked API responses
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
|
|
|
# Import internal implementation functions (not decorated tool wrappers)
|
|
from gforms.forms_tools import _batch_update_form_impl, _serialize_form_item, get_form
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_multiple_requests():
|
|
"""Test batch update with multiple requests returns formatted results"""
|
|
mock_service = Mock()
|
|
mock_response = {
|
|
"replies": [
|
|
{"createItem": {"itemId": "item001", "questionId": ["q001"]}},
|
|
{"createItem": {"itemId": "item002", "questionId": ["q002"]}},
|
|
],
|
|
"writeControl": {"requiredRevisionId": "rev123"},
|
|
}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{
|
|
"createItem": {
|
|
"item": {
|
|
"title": "What is your name?",
|
|
"questionItem": {
|
|
"question": {"textQuestion": {"paragraph": False}}
|
|
},
|
|
},
|
|
"location": {"index": 0},
|
|
}
|
|
},
|
|
{
|
|
"createItem": {
|
|
"item": {
|
|
"title": "What is your email?",
|
|
"questionItem": {
|
|
"question": {"textQuestion": {"paragraph": False}}
|
|
},
|
|
},
|
|
"location": {"index": 1},
|
|
}
|
|
},
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="test_form_123",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "Batch Update Completed" in result
|
|
assert "test_form_123" in result
|
|
assert "Requests Applied: 2" in result
|
|
assert "Replies Received: 2" in result
|
|
assert "item001" in result
|
|
assert "item002" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_single_request():
|
|
"""Test batch update with a single request"""
|
|
mock_service = Mock()
|
|
mock_response = {
|
|
"replies": [
|
|
{"createItem": {"itemId": "item001", "questionId": ["q001"]}},
|
|
],
|
|
}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{
|
|
"createItem": {
|
|
"item": {
|
|
"title": "Favourite colour?",
|
|
"questionItem": {
|
|
"question": {
|
|
"choiceQuestion": {
|
|
"type": "RADIO",
|
|
"options": [
|
|
{"value": "Red"},
|
|
{"value": "Blue"},
|
|
],
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"location": {"index": 0},
|
|
}
|
|
},
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="single_form_456",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "single_form_456" in result
|
|
assert "Requests Applied: 1" in result
|
|
assert "Replies Received: 1" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_empty_replies():
|
|
"""Test batch update when API returns no replies"""
|
|
mock_service = Mock()
|
|
mock_response = {
|
|
"replies": [],
|
|
}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{
|
|
"updateFormInfo": {
|
|
"info": {"description": "Updated description"},
|
|
"updateMask": "description",
|
|
}
|
|
},
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="info_form_789",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "info_form_789" in result
|
|
assert "Requests Applied: 1" in result
|
|
assert "Replies Received: 0" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_no_replies_key():
|
|
"""Test batch update when API response lacks replies key"""
|
|
mock_service = Mock()
|
|
mock_response = {}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{
|
|
"updateSettings": {
|
|
"settings": {"quizSettings": {"isQuiz": True}},
|
|
"updateMask": "quizSettings.isQuiz",
|
|
}
|
|
},
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="quiz_form_000",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "quiz_form_000" in result
|
|
assert "Requests Applied: 1" in result
|
|
assert "Replies Received: 0" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_url_in_response():
|
|
"""Test that the edit URL is included in the response"""
|
|
mock_service = Mock()
|
|
mock_response = {
|
|
"replies": [{}],
|
|
}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{"updateFormInfo": {"info": {"title": "New Title"}, "updateMask": "title"}}
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="url_form_abc",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "https://docs.google.com/forms/d/url_form_abc/edit" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_update_form_mixed_reply_types():
|
|
"""Test batch update with createItem replies containing different fields"""
|
|
mock_service = Mock()
|
|
mock_response = {
|
|
"replies": [
|
|
{"createItem": {"itemId": "item_a", "questionId": ["qa"]}},
|
|
{},
|
|
{"createItem": {"itemId": "item_c"}},
|
|
],
|
|
}
|
|
|
|
mock_service.forms().batchUpdate().execute.return_value = mock_response
|
|
|
|
requests = [
|
|
{"createItem": {"item": {"title": "Q1"}, "location": {"index": 0}}},
|
|
{
|
|
"updateFormInfo": {
|
|
"info": {"description": "Desc"},
|
|
"updateMask": "description",
|
|
}
|
|
},
|
|
{"createItem": {"item": {"title": "Q2"}, "location": {"index": 1}}},
|
|
]
|
|
|
|
result = await _batch_update_form_impl(
|
|
service=mock_service,
|
|
form_id="mixed_form_xyz",
|
|
requests=requests,
|
|
)
|
|
|
|
assert "Requests Applied: 3" in result
|
|
assert "Replies Received: 3" in result
|
|
assert "item_a" in result
|
|
assert "item_c" in result
|
|
|
|
|
|
def test_serialize_form_item_choice_question_includes_ids_and_options():
|
|
"""Choice question items should expose questionId/options/type metadata."""
|
|
item = {
|
|
"itemId": "item_123",
|
|
"title": "Favorite color?",
|
|
"questionItem": {
|
|
"question": {
|
|
"questionId": "q_123",
|
|
"required": True,
|
|
"choiceQuestion": {
|
|
"type": "RADIO",
|
|
"options": [{"value": "Red"}, {"value": "Blue"}],
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
serialized = _serialize_form_item(item, 1)
|
|
|
|
assert serialized["index"] == 1
|
|
assert serialized["itemId"] == "item_123"
|
|
assert serialized["type"] == "RADIO"
|
|
assert serialized["questionId"] == "q_123"
|
|
assert serialized["required"] is True
|
|
assert serialized["options"] == ["Red", "Blue"]
|
|
|
|
|
|
def test_serialize_form_item_grid_includes_row_and_column_structure():
|
|
"""Grid question groups should expose row labels/IDs and column options."""
|
|
item = {
|
|
"itemId": "grid_item_1",
|
|
"title": "Weekly chores",
|
|
"questionGroupItem": {
|
|
"questions": [
|
|
{
|
|
"questionId": "row_q1",
|
|
"required": True,
|
|
"rowQuestion": {"title": "Laundry"},
|
|
},
|
|
{
|
|
"questionId": "row_q2",
|
|
"required": False,
|
|
"rowQuestion": {"title": "Dishes"},
|
|
},
|
|
],
|
|
"grid": {
|
|
"columns": {"options": [{"value": "Never"}, {"value": "Often"}]}
|
|
},
|
|
},
|
|
}
|
|
|
|
serialized = _serialize_form_item(item, 2)
|
|
|
|
assert serialized["index"] == 2
|
|
assert serialized["type"] == "GRID"
|
|
assert serialized["grid"]["columns"] == ["Never", "Often"]
|
|
assert serialized["grid"]["rows"] == [
|
|
{"title": "Laundry", "questionId": "row_q1", "required": True},
|
|
{"title": "Dishes", "questionId": "row_q2", "required": False},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_form_returns_structured_item_metadata():
|
|
"""get_form should include question IDs, options, and grid structure."""
|
|
mock_service = Mock()
|
|
mock_service.forms().get().execute.return_value = {
|
|
"formId": "form_1",
|
|
"info": {"title": "Survey", "description": "Test survey"},
|
|
"items": [
|
|
{
|
|
"itemId": "item_1",
|
|
"title": "Favorite fruit?",
|
|
"questionItem": {
|
|
"question": {
|
|
"questionId": "q_1",
|
|
"required": True,
|
|
"choiceQuestion": {
|
|
"type": "RADIO",
|
|
"options": [{"value": "Apple"}, {"value": "Banana"}],
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"itemId": "item_2",
|
|
"title": "Household chores",
|
|
"questionGroupItem": {
|
|
"questions": [
|
|
{
|
|
"questionId": "row_1",
|
|
"required": True,
|
|
"rowQuestion": {"title": "Laundry"},
|
|
}
|
|
],
|
|
"grid": {"columns": {"options": [{"value": "Never"}]}},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
# Bypass decorators and call the core implementation directly.
|
|
result = await get_form.__wrapped__.__wrapped__(
|
|
mock_service, "user@example.com", "form_1"
|
|
)
|
|
|
|
assert "- Items (structured):" in result
|
|
assert '"questionId": "q_1"' in result
|
|
assert '"options": [' in result
|
|
assert '"Apple"' in result
|
|
assert '"type": "GRID"' in result
|
|
assert '"columns": [' in result
|
|
assert '"rows": [' in result
|