Skip to main content
Glama
test_bulk_operations.py16.8 kB
""" Tests for bulk operations functionality. Tests the bulk_update_document function and associated helper functions. """ import pytest from unittest.mock import MagicMock, patch from google_docs_mcp.api.documents import ( bulk_update_document, _prepare_insert_text_request, _prepare_delete_range_request, _prepare_insert_table_request, _prepare_insert_page_break_request, _prepare_insert_image_request, _prepare_apply_text_style_request, _prepare_apply_paragraph_style_request, ) from google_docs_mcp.api.helpers import chunk_requests from fastmcp.exceptions import ToolError class TestChunkRequests: """Tests for the chunk_requests helper function.""" def test_chunk_requests_within_limit(self): """Should not chunk when requests are within limit.""" requests = [{"request": i} for i in range(30)] chunks = chunk_requests(requests, chunk_size=50) assert len(chunks) == 1 assert len(chunks[0]) == 30 def test_chunk_requests_exactly_at_limit(self): """Should create one chunk when exactly at limit.""" requests = [{"request": i} for i in range(50)] chunks = chunk_requests(requests, chunk_size=50) assert len(chunks) == 1 assert len(chunks[0]) == 50 def test_chunk_requests_over_limit(self): """Should split into multiple chunks when over limit.""" requests = [{"request": i} for i in range(75)] chunks = chunk_requests(requests, chunk_size=50) assert len(chunks) == 2 assert len(chunks[0]) == 50 assert len(chunks[1]) == 25 def test_chunk_requests_many_chunks(self): """Should handle many chunks correctly.""" requests = [{"request": i} for i in range(123)] chunks = chunk_requests(requests, chunk_size=50) assert len(chunks) == 3 assert len(chunks[0]) == 50 assert len(chunks[1]) == 50 assert len(chunks[2]) == 23 def test_chunk_requests_empty_list(self): """Should handle empty list.""" requests = [] chunks = chunk_requests(requests, chunk_size=50) assert len(chunks) == 0 def test_chunk_requests_custom_size(self): """Should respect custom chunk size.""" requests = [{"request": i} for i in range(25)] chunks = chunk_requests(requests, chunk_size=10) assert len(chunks) == 3 assert len(chunks[0]) == 10 assert len(chunks[1]) == 10 assert len(chunks[2]) == 5 def test_chunk_requests_invalid_size(self): """Should raise error for invalid chunk size.""" requests = [{"request": i} for i in range(10)] with pytest.raises(ValueError): chunk_requests(requests, chunk_size=0) with pytest.raises(ValueError): chunk_requests(requests, chunk_size=-1) class TestPrepareInsertTextRequest: """Tests for _prepare_insert_text_request function.""" def test_basic_insert_text(self): """Should prepare basic insert text request.""" op_dict = {"text": "Hello World", "index": 1} request = _prepare_insert_text_request(op_dict, None) assert request == { "insertText": {"text": "Hello World", "location": {"index": 1}} } def test_insert_text_with_tab_id(self): """Should include tab_id when provided.""" op_dict = {"text": "Hello", "index": 5, "tab_id": "tab123"} request = _prepare_insert_text_request(op_dict, None) assert request["insertText"]["location"]["tabId"] == "tab123" def test_insert_text_with_default_tab_id(self): """Should use default tab_id when not specified in operation.""" op_dict = {"text": "Hello", "index": 5} request = _prepare_insert_text_request(op_dict, "default_tab") assert request["insertText"]["location"]["tabId"] == "default_tab" def test_insert_text_operation_overrides_default(self): """Should use operation tab_id over default.""" op_dict = {"text": "Hello", "index": 5, "tab_id": "op_tab"} request = _prepare_insert_text_request(op_dict, "default_tab") assert request["insertText"]["location"]["tabId"] == "op_tab" class TestPrepareDeleteRangeRequest: """Tests for _prepare_delete_range_request function.""" def test_basic_delete_range(self): """Should prepare basic delete range request.""" op_dict = {"start_index": 1, "end_index": 10} request = _prepare_delete_range_request(op_dict, None) assert request == { "deleteContentRange": { "range": {"startIndex": 1, "endIndex": 10} } } def test_delete_range_with_tab_id(self): """Should include tab_id when provided.""" op_dict = {"start_index": 1, "end_index": 10, "tab_id": "tab123"} request = _prepare_delete_range_request(op_dict, None) assert request["deleteContentRange"]["range"]["tabId"] == "tab123" def test_delete_range_invalid_indices(self): """Should raise error when end_index <= start_index.""" op_dict = {"start_index": 10, "end_index": 10} with pytest.raises(ToolError) as exc_info: _prepare_delete_range_request(op_dict, None) assert "end_index" in str(exc_info.value) assert "start_index" in str(exc_info.value) def test_delete_range_reversed_indices(self): """Should raise error when indices are reversed.""" op_dict = {"start_index": 20, "end_index": 10} with pytest.raises(ToolError): _prepare_delete_range_request(op_dict, None) class TestPrepareInsertTableRequest: """Tests for _prepare_insert_table_request function.""" def test_basic_insert_table(self): """Should prepare basic table insert request.""" op_dict = {"rows": 3, "columns": 2, "index": 1} request = _prepare_insert_table_request(op_dict) assert request == { "insertTable": {"rows": 3, "columns": 2, "location": {"index": 1}} } def test_insert_table_invalid_rows(self): """Should raise error for invalid row count.""" op_dict = {"rows": 0, "columns": 2, "index": 1} with pytest.raises(ToolError) as exc_info: _prepare_insert_table_request(op_dict) assert "at least 1 row" in str(exc_info.value) def test_insert_table_invalid_columns(self): """Should raise error for invalid column count.""" op_dict = {"rows": 2, "columns": 0, "index": 1} with pytest.raises(ToolError) as exc_info: _prepare_insert_table_request(op_dict) assert "at least 1" in str(exc_info.value) class TestPrepareInsertPageBreakRequest: """Tests for _prepare_insert_page_break_request function.""" def test_basic_insert_page_break(self): """Should prepare page break insert request.""" op_dict = {"index": 10} request = _prepare_insert_page_break_request(op_dict) assert request == {"insertPageBreak": {"location": {"index": 10}}} class TestPrepareInsertImageRequest: """Tests for _prepare_insert_image_request function.""" @patch('urllib.request.urlopen') def test_basic_insert_image(self, mock_urlopen): """Should prepare basic image insert request.""" # Mock URL validation mock_response = MagicMock() mock_response.getcode.return_value = 200 mock_response.headers.get.return_value = 'image/png' mock_urlopen.return_value.__enter__.return_value = mock_response op_dict = { "image_url": "https://example.com/image.png", "index": 1, } request = _prepare_insert_image_request(op_dict) assert request["insertInlineImage"]["uri"] == "https://example.com/image.png" assert request["insertInlineImage"]["location"]["index"] == 1 assert "objectSize" not in request["insertInlineImage"] @patch('urllib.request.urlopen') def test_insert_image_with_dimensions(self, mock_urlopen): """Should include dimensions when provided.""" # Mock URL validation mock_response = MagicMock() mock_response.getcode.return_value = 200 mock_response.headers.get.return_value = 'image/png' mock_urlopen.return_value.__enter__.return_value = mock_response op_dict = { "image_url": "https://example.com/image.png", "index": 1, "width": 200, "height": 150, } request = _prepare_insert_image_request(op_dict) assert "objectSize" in request["insertInlineImage"] assert request["insertInlineImage"]["objectSize"]["width"]["magnitude"] == 200 assert request["insertInlineImage"]["objectSize"]["height"]["magnitude"] == 150 def test_insert_image_missing_url(self): """Should raise error when image_url is missing.""" op_dict = {"index": 1} with pytest.raises(ToolError) as exc_info: _prepare_insert_image_request(op_dict) assert "image_url is required" in str(exc_info.value) def test_insert_image_invalid_url(self): """Should raise error for invalid URL.""" op_dict = {"image_url": "not-a-url", "index": 1} with pytest.raises(ToolError) as exc_info: _prepare_insert_image_request(op_dict) assert "Invalid image URL" in str(exc_info.value) class TestBulkUpdateDocument: """Tests for bulk_update_document function.""" @patch("google_docs_mcp.api.documents.get_docs_client") def test_empty_operations(self, mock_get_docs): """Should handle empty operations list.""" result = bulk_update_document("doc123", []) assert "No operations" in result mock_get_docs.assert_called_once() @patch("google_docs_mcp.api.documents.get_docs_client") def test_too_many_operations(self, mock_get_docs): """Should reject too many operations.""" operations = [{"type": "insert_text", "text": "x", "index": 1}] * 501 with pytest.raises(ToolError) as exc_info: bulk_update_document("doc123", operations) assert "Too many operations" in str(exc_info.value) assert "500" in str(exc_info.value) @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_single_insert_text_operation(self, mock_get_docs, mock_execute_batch): """Should execute single insert_text operation.""" mock_execute_batch.return_value = {} operations = [{"type": "insert_text", "text": "Hello", "index": 1}] result = bulk_update_document("doc123", operations) assert "Successfully executed 1 operations" in result assert "1× insert_text" in result mock_execute_batch.assert_called_once() # Verify the request structure call_args = mock_execute_batch.call_args requests = call_args[0][2] # Third argument is the requests list assert len(requests) == 1 assert "insertText" in requests[0] @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_multiple_mixed_operations(self, mock_get_docs, mock_execute_batch): """Should execute multiple mixed operations.""" mock_execute_batch.return_value = {} operations = [ {"type": "insert_text", "text": "Title\n", "index": 1}, {"type": "insert_table", "rows": 2, "columns": 3, "index": 7}, {"type": "insert_page_break", "index": 20}, ] result = bulk_update_document("doc123", operations) assert "Successfully executed 3 operations" in result assert "1× insert_page_break" in result assert "1× insert_table" in result assert "1× insert_text" in result # Verify batch update was called once (all operations fit in one batch) mock_execute_batch.assert_called_once() # Verify the requests call_args = mock_execute_batch.call_args requests = call_args[0][2] assert len(requests) == 3 @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_operations_exceeding_batch_limit(self, mock_get_docs, mock_execute_batch): """Should split operations into multiple batches when exceeding 50 requests.""" mock_execute_batch.return_value = {} # Create 75 operations operations = [ {"type": "insert_text", "text": f"Text {i}", "index": i + 1} for i in range(75) ] result = bulk_update_document("doc123", operations) assert "Successfully executed 75 operations" in result assert "2 batch(es)" in result # Verify batch update was called twice assert mock_execute_batch.call_count == 2 # First batch should have 50 requests first_call_requests = mock_execute_batch.call_args_list[0][0][2] assert len(first_call_requests) == 50 # Second batch should have 25 requests second_call_requests = mock_execute_batch.call_args_list[1][0][2] assert len(second_call_requests) == 25 @patch("google_docs_mcp.api.documents.get_docs_client") def test_unknown_operation_type(self, mock_get_docs): """Should raise error for unknown operation type.""" operations = [{"type": "unknown_operation", "data": "something"}] with pytest.raises(ToolError) as exc_info: bulk_update_document("doc123", operations) assert "Unknown operation type" in str(exc_info.value) assert "unknown_operation" in str(exc_info.value) @patch("google_docs_mcp.api.documents.get_docs_client") def test_missing_operation_type(self, mock_get_docs): """Should raise error when operation is missing type field.""" operations = [{"text": "Hello", "index": 1}] with pytest.raises(ToolError) as exc_info: bulk_update_document("doc123", operations) assert "missing 'type' field" in str(exc_info.value) @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_operation_with_default_tab_id(self, mock_get_docs, mock_execute_batch): """Should apply default tab_id to operations without explicit tab_id.""" mock_execute_batch.return_value = {} operations = [{"type": "insert_text", "text": "Hello", "index": 1}] result = bulk_update_document("doc123", operations, default_tab_id="tab_default") # Verify the request includes the default tab_id call_args = mock_execute_batch.call_args requests = call_args[0][2] assert requests[0]["insertText"]["location"]["tabId"] == "tab_default" @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_delete_range_operation(self, mock_get_docs, mock_execute_batch): """Should execute delete_range operation.""" mock_execute_batch.return_value = {} operations = [{"type": "delete_range", "start_index": 5, "end_index": 15}] result = bulk_update_document("doc123", operations) assert "Successfully executed 1 operations" in result assert "1× delete_range" in result # Verify the request call_args = mock_execute_batch.call_args requests = call_args[0][2] assert "deleteContentRange" in requests[0] assert requests[0]["deleteContentRange"]["range"]["startIndex"] == 5 assert requests[0]["deleteContentRange"]["range"]["endIndex"] == 15 @patch("google_docs_mcp.api.documents.helpers.execute_batch_update_sync") @patch("google_docs_mcp.api.documents.get_docs_client") def test_operation_count_grouping(self, mock_get_docs, mock_execute_batch): """Should correctly group and count operation types in summary.""" mock_execute_batch.return_value = {} operations = [ {"type": "insert_text", "text": "A", "index": 1}, {"type": "insert_text", "text": "B", "index": 2}, {"type": "insert_text", "text": "C", "index": 3}, {"type": "insert_table", "rows": 2, "columns": 2, "index": 10}, {"type": "insert_table", "rows": 3, "columns": 3, "index": 20}, ] result = bulk_update_document("doc123", operations) assert "Successfully executed 5 operations" in result assert "3× insert_text" in result assert "2× insert_table" in result

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nickweedon/google-docs-mcp-docker'

If you have feedback or need assistance with the MCP directory API, please join our Discord server