Files
mcp-server/tests/integration/test_files_mcp_routes.py
T
ClaudeandClaude Fable 5 8c110329b2 feat(files-mcp): FastAPI app with 9 tool routes and integration tests
Adds files_mcp/server.py (FastAPI title=Files MCP, 9 POST routes, and
3 exception handlers: KeyError->404, ValueError->400, FileExistsError->409).
The third handler is the only one not present in spark_executor.server;
handlers are registered on this app only, so spark_executor is unaffected.

Adds tests/integration/test_files_mcp_routes.py with 12 end-to-end HTTP
tests covering CRUD happy paths, error codes (404/400/409), and
introspection (list_dir, stat, search, move, copy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-30 18:09:35 +08:00

142 lines
5.6 KiB
Python

# coding=utf-8
"""
@Time :2026/6/30
@Author :tao.chen
End-to-end HTTP tests for files_mcp.server.app. Uses FastAPI TestClient
directly (no MCP transport) — same pattern as
tests/integration/test_mcp_routes.py.
"""
from fastapi.testclient import TestClient
from files_mcp.core import path_guard
from files_mcp.server import app
def _client(tmp_path) -> TestClient:
import os
os.environ["FILES_MCP_ROOT"] = str(tmp_path)
path_guard.set_root_for_testing(tmp_path.resolve())
return TestClient(app)
class TestCrud:
def test_create_then_read(self, tmp_path):
with _client(tmp_path) as c:
r = c.post("/create_file", json={"path": "a.txt", "content": "hello"})
assert r.status_code == 200
assert r.json() == {"path": str(tmp_path / "a.txt"), "status": "CREATED", "size": 5}
r = c.post("/read_file", json={"path": "a.txt"})
assert r.status_code == 200
assert r.json()["content"] == "hello"
def test_update_existing(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "old"})
r = c.post("/update_file", json={"path": "a.txt", "content": "new"})
assert r.status_code == 200
assert r.json()["status"] == "UPDATED"
r = c.post("/read_file", json={"path": "a.txt"})
assert r.json()["content"] == "new"
def test_delete(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "x"})
r = c.post("/delete_file", json={"path": "a.txt"})
assert r.status_code == 200
assert r.json()["status"] == "DELETED"
r = c.post("/read_file", json={"path": "a.txt"})
assert r.status_code == 404
class TestErrors:
def test_create_existing_returns_409(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "x"})
r = c.post("/create_file", json={"path": "a.txt", "content": "y"})
assert r.status_code == 409
def test_update_missing_returns_404(self, tmp_path):
with _client(tmp_path) as c:
r = c.post("/update_file", json={"path": "nope.txt", "content": "x"})
assert r.status_code == 404
def test_escape_returns_400(self, tmp_path):
with _client(tmp_path) as c:
r = c.post("/read_file", json={"path": "/etc/passwd"})
assert r.status_code == 400
assert "escapes sandbox" in r.json()["detail"]
def test_too_large_returns_400(self, tmp_path):
with _client(tmp_path) as c:
r = c.post(
"/create_file",
json={"path": "big.txt", "content": "x" * (1024 * 1024 + 1)},
)
assert r.status_code == 400
assert "too large" in r.json()["detail"]
def test_non_utf8_returns_400(self, tmp_path):
with _client(tmp_path) as c:
(tmp_path / "bin").write_bytes(b"\xff\xfe\x00")
r = c.post("/read_file", json={"path": "bin"})
assert r.status_code == 400
assert "utf-8" in r.json()["detail"]
class TestIntrospection:
def test_list_dir(self, tmp_path):
with _client(tmp_path) as c:
(tmp_path / "a.txt").write_text("a", encoding="utf-8")
(tmp_path / "b").mkdir()
r = c.post("/list_dir", json={"path": ".", "recursive": False, "max_depth": 1})
assert r.status_code == 200
names = sorted(e["name"] for e in r.json()["entries"])
assert names == ["a.txt", "b"]
def test_stat(self, tmp_path):
with _client(tmp_path) as c:
(tmp_path / "f.txt").write_text("hi", encoding="utf-8")
r = c.post("/stat", json={"path": "f.txt"})
assert r.status_code == 200
assert r.json()["type"] == "file"
assert r.json()["size"] == 2
def test_search(self, tmp_path):
with _client(tmp_path) as c:
(tmp_path / "a.py").write_text("a", encoding="utf-8")
(tmp_path / "b.txt").write_text("b", encoding="utf-8")
r = c.post("/search", json={"path": ".", "pattern": "*.py", "recursive": False})
assert r.status_code == 200
assert len(r.json()["matches"]) == 1
class TestMoveCopy:
def test_move(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "x"})
r = c.post("/move", json={"src": "a.txt", "dst": "b.txt"})
assert r.status_code == 200
assert r.json()["status"] == "MOVED"
r = c.post("/read_file", json={"path": "b.txt"})
assert r.json()["content"] == "x"
def test_move_existing_dst_409(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "x"})
c.post("/create_file", json={"path": "b.txt", "content": "y"})
r = c.post("/move", json={"src": "a.txt", "dst": "b.txt"})
assert r.status_code == 409
def test_copy(self, tmp_path):
with _client(tmp_path) as c:
c.post("/create_file", json={"path": "a.txt", "content": "x"})
r = c.post("/copy", json={"src": "a.txt", "dst": "b.txt"})
assert r.status_code == 200
assert r.json()["status"] == "COPIED"
# source unchanged
r = c.post("/read_file", json={"path": "a.txt"})
assert r.json()["content"] == "x"
r = c.post("/read_file", json={"path": "b.txt"})
assert r.json()["content"] == "x"