feat: add mock Grist server for integration testing
This commit is contained in:
13
tests/integration/mock_grist/Dockerfile
Normal file
13
tests/integration/mock_grist/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server.py .
|
||||
|
||||
ENV PORT=8484
|
||||
EXPOSE 8484
|
||||
|
||||
CMD ["python", "server.py"]
|
||||
0
tests/integration/mock_grist/__init__.py
Normal file
0
tests/integration/mock_grist/__init__.py
Normal file
2
tests/integration/mock_grist/requirements.txt
Normal file
2
tests/integration/mock_grist/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
starlette>=0.41.0
|
||||
uvicorn>=0.32.0
|
||||
217
tests/integration/mock_grist/server.py
Normal file
217
tests/integration/mock_grist/server.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""Mock Grist API server for integration testing."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [MOCK-GRIST] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Mock data
|
||||
MOCK_TABLES = {
|
||||
"People": {
|
||||
"columns": [
|
||||
{"id": "Name", "fields": {"type": "Text"}},
|
||||
{"id": "Age", "fields": {"type": "Int"}},
|
||||
{"id": "Email", "fields": {"type": "Text"}},
|
||||
],
|
||||
"records": [
|
||||
{"id": 1, "fields": {"Name": "Alice", "Age": 30, "Email": "alice@example.com"}},
|
||||
{"id": 2, "fields": {"Name": "Bob", "Age": 25, "Email": "bob@example.com"}},
|
||||
],
|
||||
},
|
||||
"Tasks": {
|
||||
"columns": [
|
||||
{"id": "Title", "fields": {"type": "Text"}},
|
||||
{"id": "Done", "fields": {"type": "Bool"}},
|
||||
],
|
||||
"records": [
|
||||
{"id": 1, "fields": {"Title": "Write tests", "Done": False}},
|
||||
{"id": 2, "fields": {"Title": "Deploy", "Done": False}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# Track requests for test assertions
|
||||
request_log: list[dict] = []
|
||||
|
||||
|
||||
def log_request(method: str, path: str, body: dict | None = None):
|
||||
"""Log a request for later inspection."""
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"method": method,
|
||||
"path": path,
|
||||
"body": body,
|
||||
}
|
||||
request_log.append(entry)
|
||||
logger.info(f"{method} {path}" + (f" body={json.dumps(body)}" if body else ""))
|
||||
|
||||
|
||||
async def health(request):
|
||||
"""Health check endpoint."""
|
||||
return JSONResponse({"status": "ok"})
|
||||
|
||||
|
||||
async def get_request_log(request):
|
||||
"""Return the request log for test assertions."""
|
||||
return JSONResponse(request_log)
|
||||
|
||||
|
||||
async def clear_request_log(request):
|
||||
"""Clear the request log."""
|
||||
request_log.clear()
|
||||
return JSONResponse({"status": "cleared"})
|
||||
|
||||
|
||||
async def list_tables(request):
|
||||
"""GET /api/docs/{doc_id}/tables"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
log_request("GET", f"/api/docs/{doc_id}/tables")
|
||||
tables = [{"id": name} for name in MOCK_TABLES.keys()]
|
||||
return JSONResponse({"tables": tables})
|
||||
|
||||
|
||||
async def get_table_columns(request):
|
||||
"""GET /api/docs/{doc_id}/tables/{table_id}/columns"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
log_request("GET", f"/api/docs/{doc_id}/tables/{table_id}/columns")
|
||||
|
||||
if table_id not in MOCK_TABLES:
|
||||
return JSONResponse({"error": "Table not found"}, status_code=404)
|
||||
|
||||
return JSONResponse({"columns": MOCK_TABLES[table_id]["columns"]})
|
||||
|
||||
|
||||
async def get_records(request):
|
||||
"""GET /api/docs/{doc_id}/tables/{table_id}/records"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
log_request("GET", f"/api/docs/{doc_id}/tables/{table_id}/records")
|
||||
|
||||
if table_id not in MOCK_TABLES:
|
||||
return JSONResponse({"error": "Table not found"}, status_code=404)
|
||||
|
||||
return JSONResponse({"records": MOCK_TABLES[table_id]["records"]})
|
||||
|
||||
|
||||
async def add_records(request):
|
||||
"""POST /api/docs/{doc_id}/tables/{table_id}/records"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
body = await request.json()
|
||||
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/records", body)
|
||||
|
||||
# Return mock IDs for new records
|
||||
new_ids = [{"id": 100 + i} for i in range(len(body.get("records", [])))]
|
||||
return JSONResponse({"records": new_ids})
|
||||
|
||||
|
||||
async def update_records(request):
|
||||
"""PATCH /api/docs/{doc_id}/tables/{table_id}/records"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
body = await request.json()
|
||||
log_request("PATCH", f"/api/docs/{doc_id}/tables/{table_id}/records", body)
|
||||
return JSONResponse({})
|
||||
|
||||
|
||||
async def delete_records(request):
|
||||
"""POST /api/docs/{doc_id}/tables/{table_id}/data/delete"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
body = await request.json()
|
||||
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/data/delete", body)
|
||||
return JSONResponse({})
|
||||
|
||||
|
||||
async def sql_query(request):
|
||||
"""GET /api/docs/{doc_id}/sql"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
query = request.query_params.get("q", "")
|
||||
log_request("GET", f"/api/docs/{doc_id}/sql?q={query}")
|
||||
|
||||
# Return mock SQL results
|
||||
return JSONResponse({
|
||||
"records": [
|
||||
{"fields": {"Name": "Alice", "Age": 30}},
|
||||
{"fields": {"Name": "Bob", "Age": 25}},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
async def create_tables(request):
|
||||
"""POST /api/docs/{doc_id}/tables"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
body = await request.json()
|
||||
log_request("POST", f"/api/docs/{doc_id}/tables", body)
|
||||
|
||||
# Return the created tables with their IDs
|
||||
tables = [{"id": t["id"]} for t in body.get("tables", [])]
|
||||
return JSONResponse({"tables": tables})
|
||||
|
||||
|
||||
async def add_column(request):
|
||||
"""POST /api/docs/{doc_id}/tables/{table_id}/columns"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
body = await request.json()
|
||||
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/columns", body)
|
||||
|
||||
columns = [{"id": c["id"]} for c in body.get("columns", [])]
|
||||
return JSONResponse({"columns": columns})
|
||||
|
||||
|
||||
async def modify_column(request):
|
||||
"""PATCH /api/docs/{doc_id}/tables/{table_id}/columns/{col_id}"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
col_id = request.path_params["col_id"]
|
||||
body = await request.json()
|
||||
log_request("PATCH", f"/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", body)
|
||||
return JSONResponse({})
|
||||
|
||||
|
||||
async def delete_column(request):
|
||||
"""DELETE /api/docs/{doc_id}/tables/{table_id}/columns/{col_id}"""
|
||||
doc_id = request.path_params["doc_id"]
|
||||
table_id = request.path_params["table_id"]
|
||||
col_id = request.path_params["col_id"]
|
||||
log_request("DELETE", f"/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}")
|
||||
return JSONResponse({})
|
||||
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
# Test control endpoints
|
||||
Route("/health", endpoint=health),
|
||||
Route("/_test/requests", endpoint=get_request_log),
|
||||
Route("/_test/requests/clear", endpoint=clear_request_log, methods=["POST"]),
|
||||
|
||||
# Grist API endpoints
|
||||
Route("/api/docs/{doc_id}/tables", endpoint=list_tables),
|
||||
Route("/api/docs/{doc_id}/tables", endpoint=create_tables, methods=["POST"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/columns", endpoint=get_table_columns),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/columns", endpoint=add_column, methods=["POST"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", endpoint=modify_column, methods=["PATCH"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", endpoint=delete_column, methods=["DELETE"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=get_records),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=add_records, methods=["POST"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=update_records, methods=["PATCH"]),
|
||||
Route("/api/docs/{doc_id}/tables/{table_id}/data/delete", endpoint=delete_records, methods=["POST"]),
|
||||
Route("/api/docs/{doc_id}/sql", endpoint=sql_query),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
port = int(os.environ.get("PORT", "8484"))
|
||||
logger.info(f"Starting mock Grist server on port {port}")
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
Reference in New Issue
Block a user