- Add get-test-instance-id.sh for branch-based container isolation - Add run-integration-tests.sh for full test lifecycle management - Update integration tests to read service URLs from environment variables (GRIST_MCP_URL, MOCK_GRIST_URL) with fallback defaults
224 lines
7.8 KiB
Python
224 lines
7.8 KiB
Python
"""Test tool calls through MCP client to verify Grist API interactions."""
|
|
|
|
import json
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
import httpx
|
|
import pytest
|
|
from mcp import ClientSession
|
|
from mcp.client.sse import sse_client
|
|
|
|
|
|
GRIST_MCP_URL = os.environ.get("GRIST_MCP_URL", "http://localhost:3000")
|
|
MOCK_GRIST_URL = os.environ.get("MOCK_GRIST_URL", "http://localhost:8484")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def create_mcp_session():
|
|
"""Create and yield an MCP session."""
|
|
async with sse_client(f"{GRIST_MCP_URL}/sse") as (read_stream, write_stream):
|
|
async with ClientSession(read_stream, write_stream) as session:
|
|
await session.initialize()
|
|
yield session
|
|
|
|
|
|
def get_mock_request_log():
|
|
"""Get the request log from mock Grist server."""
|
|
with httpx.Client(base_url=MOCK_GRIST_URL, timeout=10.0) as client:
|
|
return client.get("/_test/requests").json()
|
|
|
|
|
|
def clear_mock_request_log():
|
|
"""Clear the mock Grist request log."""
|
|
with httpx.Client(base_url=MOCK_GRIST_URL, timeout=10.0) as client:
|
|
client.post("/_test/requests/clear")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_tools(services_ready):
|
|
"""Test all MCP tools - reads, writes, schema ops, and auth errors."""
|
|
async with create_mcp_session() as client:
|
|
# ===== READ TOOLS =====
|
|
|
|
# Test list_documents
|
|
clear_mock_request_log()
|
|
result = await client.call_tool("list_documents", {})
|
|
assert len(result.content) == 1
|
|
data = json.loads(result.content[0].text)
|
|
assert "documents" in data
|
|
assert len(data["documents"]) == 1
|
|
assert data["documents"][0]["name"] == "test-doc"
|
|
assert "read" in data["documents"][0]["permissions"]
|
|
|
|
# Test list_tables
|
|
clear_mock_request_log()
|
|
result = await client.call_tool("list_tables", {"document": "test-doc"})
|
|
data = json.loads(result.content[0].text)
|
|
assert "tables" in data
|
|
assert "People" in data["tables"]
|
|
assert "Tasks" in data["tables"]
|
|
log = get_mock_request_log()
|
|
assert any("/tables" in entry["path"] for entry in log)
|
|
|
|
# Test describe_table
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"describe_table",
|
|
{"document": "test-doc", "table": "People"}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "columns" in data
|
|
column_ids = [c["id"] for c in data["columns"]]
|
|
assert "Name" in column_ids
|
|
assert "Age" in column_ids
|
|
log = get_mock_request_log()
|
|
assert any("/columns" in entry["path"] for entry in log)
|
|
|
|
# Test get_records
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"get_records",
|
|
{"document": "test-doc", "table": "People"}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "records" in data
|
|
assert len(data["records"]) == 2
|
|
assert data["records"][0]["Name"] == "Alice"
|
|
log = get_mock_request_log()
|
|
assert any("/records" in entry["path"] and entry["method"] == "GET" for entry in log)
|
|
|
|
# Test sql_query
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"sql_query",
|
|
{"document": "test-doc", "query": "SELECT Name, Age FROM People"}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "records" in data
|
|
assert len(data["records"]) >= 1
|
|
log = get_mock_request_log()
|
|
assert any("/sql" in entry["path"] for entry in log)
|
|
|
|
# ===== WRITE TOOLS =====
|
|
|
|
# Test add_records
|
|
clear_mock_request_log()
|
|
new_records = [
|
|
{"Name": "Charlie", "Age": 35, "Email": "charlie@example.com"}
|
|
]
|
|
result = await client.call_tool(
|
|
"add_records",
|
|
{"document": "test-doc", "table": "People", "records": new_records}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "inserted_ids" in data
|
|
assert len(data["inserted_ids"]) == 1
|
|
log = get_mock_request_log()
|
|
post_requests = [e for e in log if e["method"] == "POST" and "/records" in e["path"]]
|
|
assert len(post_requests) >= 1
|
|
assert post_requests[-1]["body"]["records"][0]["fields"]["Name"] == "Charlie"
|
|
|
|
# Test update_records
|
|
clear_mock_request_log()
|
|
updates = [{"id": 1, "fields": {"Age": 31}}]
|
|
result = await client.call_tool(
|
|
"update_records",
|
|
{"document": "test-doc", "table": "People", "records": updates}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "updated" in data
|
|
log = get_mock_request_log()
|
|
patch_requests = [e for e in log if e["method"] == "PATCH" and "/records" in e["path"]]
|
|
assert len(patch_requests) >= 1
|
|
|
|
# Test delete_records
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"delete_records",
|
|
{"document": "test-doc", "table": "People", "record_ids": [1, 2]}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "deleted" in data
|
|
log = get_mock_request_log()
|
|
delete_requests = [e for e in log if "/data/delete" in e["path"]]
|
|
assert len(delete_requests) >= 1
|
|
assert delete_requests[-1]["body"] == [1, 2]
|
|
|
|
# ===== SCHEMA TOOLS =====
|
|
|
|
# Test create_table
|
|
clear_mock_request_log()
|
|
columns = [
|
|
{"id": "Title", "type": "Text"},
|
|
{"id": "Count", "type": "Int"},
|
|
]
|
|
result = await client.call_tool(
|
|
"create_table",
|
|
{"document": "test-doc", "table_id": "NewTable", "columns": columns}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "table_id" in data
|
|
log = get_mock_request_log()
|
|
post_tables = [e for e in log if e["method"] == "POST" and e["path"].endswith("/tables")]
|
|
assert len(post_tables) >= 1
|
|
|
|
# Test add_column
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"add_column",
|
|
{
|
|
"document": "test-doc",
|
|
"table": "People",
|
|
"column_id": "Phone",
|
|
"column_type": "Text",
|
|
}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "column_id" in data
|
|
log = get_mock_request_log()
|
|
post_cols = [e for e in log if e["method"] == "POST" and "/columns" in e["path"]]
|
|
assert len(post_cols) >= 1
|
|
|
|
# Test modify_column
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"modify_column",
|
|
{
|
|
"document": "test-doc",
|
|
"table": "People",
|
|
"column_id": "Age",
|
|
"type": "Numeric",
|
|
}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "modified" in data
|
|
log = get_mock_request_log()
|
|
patch_cols = [e for e in log if e["method"] == "PATCH" and "/columns/" in e["path"]]
|
|
assert len(patch_cols) >= 1
|
|
|
|
# Test delete_column
|
|
clear_mock_request_log()
|
|
result = await client.call_tool(
|
|
"delete_column",
|
|
{
|
|
"document": "test-doc",
|
|
"table": "People",
|
|
"column_id": "Email",
|
|
}
|
|
)
|
|
data = json.loads(result.content[0].text)
|
|
assert "deleted" in data
|
|
log = get_mock_request_log()
|
|
delete_cols = [e for e in log if e["method"] == "DELETE" and "/columns/" in e["path"]]
|
|
assert len(delete_cols) >= 1
|
|
|
|
# ===== AUTHORIZATION =====
|
|
|
|
# Test unauthorized document fails
|
|
result = await client.call_tool(
|
|
"list_tables",
|
|
{"document": "unauthorized-doc"}
|
|
)
|
|
assert "error" in result.content[0].text.lower() or "authorization" in result.content[0].text.lower()
|