285 lines
7.8 KiB
Python
285 lines
7.8 KiB
Python
import sys
|
||
import pytest
|
||
from backend.agent.servers import mcp_server_file_search as server
|
||
|
||
|
||
# =========================================================
|
||
# FIXTURES
|
||
# =========================================================
|
||
|
||
@pytest.fixture()
|
||
def workspace(tmp_path, monkeypatch):
|
||
"""
|
||
Erstellt einen isolierten Workspace für jeden Test.
|
||
"""
|
||
ws = tmp_path / "workspace"
|
||
ws.mkdir()
|
||
|
||
monkeypatch.setattr(server, "ALLOWED_DIR", ws)
|
||
|
||
return ws
|
||
|
||
|
||
# =========================================================
|
||
# BASIC TESTS (1–10)
|
||
# =========================================================
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 1. _safe_path erlaubt gültige Pfade
|
||
# ---------------------------------------------------------
|
||
|
||
def test_safe_path_valid(workspace):
|
||
result = server._safe_path("test.txt")
|
||
|
||
assert result == workspace / "test.txt"
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 2. _safe_path blockiert Path Traversal
|
||
# ---------------------------------------------------------
|
||
|
||
def test_safe_path_blocks_traversal(workspace):
|
||
with pytest.raises(ValueError):
|
||
server._safe_path("../secret.txt")
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 3. list_files liefert leeren Hinweis
|
||
# ---------------------------------------------------------
|
||
|
||
def test_list_files_empty(workspace):
|
||
result = server.list_files()
|
||
|
||
assert result == "No files found in the project directory."
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 4. list_files findet Dateien rekursiv
|
||
# ---------------------------------------------------------
|
||
|
||
def test_list_files_recursive(workspace):
|
||
src = workspace / "src"
|
||
src.mkdir()
|
||
|
||
(src / "main.py").write_text("print('hello')")
|
||
|
||
result = server.list_files()
|
||
|
||
# list_files() uses Path.relative_to() which yields OS-native separators;
|
||
# check for the components instead of a hard-coded slash style.
|
||
assert "src" in result and "main.py" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 5. read_file liest Datei korrekt
|
||
# ---------------------------------------------------------
|
||
|
||
def test_read_file_success(workspace):
|
||
file = workspace / "hello.txt"
|
||
file.write_text("Hello World")
|
||
|
||
result = server.read_file("hello.txt")
|
||
|
||
assert result == "Hello World"
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 6. read_file erkennt fehlende Datei
|
||
# ---------------------------------------------------------
|
||
|
||
def test_read_file_missing(workspace):
|
||
result = server.read_file("missing.txt")
|
||
|
||
assert "does not exist" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 7. write_new_file erstellt Datei
|
||
# ---------------------------------------------------------
|
||
|
||
def test_write_new_file_success(workspace):
|
||
result = server.write_new_file("new.txt", "content")
|
||
|
||
assert "OK:" in result
|
||
assert (workspace / "new.txt").exists()
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 8. write_new_file verhindert Überschreiben
|
||
# ---------------------------------------------------------
|
||
|
||
def test_write_new_file_existing(workspace):
|
||
file = workspace / "exists.txt"
|
||
file.write_text("old")
|
||
|
||
result = server.write_new_file("exists.txt", "new")
|
||
|
||
assert "already exists" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 9. create_new_directory erstellt Verzeichnis
|
||
# ---------------------------------------------------------
|
||
|
||
def test_create_new_directory_success(workspace):
|
||
result = server.create_new_directory("mydir")
|
||
|
||
assert "OK:" in result
|
||
assert (workspace / "mydir").is_dir()
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 10. search_files findet Inhalte
|
||
# ---------------------------------------------------------
|
||
|
||
def test_search_files_content_match(workspace):
|
||
file = workspace / "notes.txt"
|
||
file.write_text("Python MCP Server")
|
||
|
||
result = server.search_files("mcp")
|
||
|
||
assert "[content]" in result
|
||
|
||
|
||
# =========================================================
|
||
# EDGE CASE TESTS (11–20)
|
||
# =========================================================
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 11. Mehrfaches Traversal blockieren
|
||
# ---------------------------------------------------------
|
||
|
||
def test_safe_path_double_traversal(workspace):
|
||
with pytest.raises(ValueError):
|
||
server._safe_path("../../../../etc/passwd")
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 12. Symlink Escape verhindern
|
||
# ---------------------------------------------------------
|
||
|
||
@pytest.mark.skipif(sys.platform == "win32", reason="Symlinks require special privileges on Windows")
|
||
def test_safe_path_symlink_escape(workspace):
|
||
outside = workspace.parent / "outside"
|
||
outside.mkdir()
|
||
|
||
target = outside / "evil.txt"
|
||
target.write_text("bad")
|
||
|
||
link = workspace / "link"
|
||
link.symlink_to(outside)
|
||
|
||
with pytest.raises(ValueError):
|
||
server._safe_path("link/evil.txt")
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 13. Dateien ohne Extension blockieren
|
||
# ---------------------------------------------------------
|
||
|
||
def test_write_file_without_extension(workspace):
|
||
result = server.write_new_file("README", "test")
|
||
|
||
assert "can only write" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 14. Hidden Files blockieren
|
||
# ---------------------------------------------------------
|
||
|
||
def test_write_hidden_file(workspace):
|
||
result = server.write_new_file(".env", "SECRET=123")
|
||
|
||
assert "can only write" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 15. Binary Files korrekt behandeln
|
||
# ---------------------------------------------------------
|
||
|
||
def test_read_binary_file(workspace):
|
||
binary = workspace / "data.bin"
|
||
binary.write_bytes(b"\xFF\xFE\xFD")
|
||
|
||
result = server.read_file("data.bin")
|
||
|
||
assert "not a text file" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 16. Sehr große Zeilen durchsuchen
|
||
# ---------------------------------------------------------
|
||
|
||
def test_search_huge_line(workspace):
|
||
huge_text = "A" * 1_000_000 + "needle"
|
||
|
||
file = workspace / "huge.txt"
|
||
file.write_text(huge_text)
|
||
|
||
result = server.search_files("needle")
|
||
|
||
assert "[content]" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 17. Leere Dateien lesen
|
||
# ---------------------------------------------------------
|
||
|
||
def test_read_empty_file(workspace):
|
||
file = workspace / "empty.txt"
|
||
file.write_text("")
|
||
|
||
result = server.read_file("empty.txt")
|
||
|
||
assert result == ""
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 18. Sonderzeichen im Query
|
||
# ---------------------------------------------------------
|
||
|
||
def test_search_special_characters(workspace):
|
||
file = workspace / "test.txt"
|
||
file.write_text("hello [world] (test)")
|
||
|
||
result = server.search_files("[world]")
|
||
|
||
assert "[content]" in result
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 19. Unicode-Dateinamen unterstützen
|
||
# ---------------------------------------------------------
|
||
|
||
def test_write_unicode_filename(workspace):
|
||
filename = "🔥_überraschung.txt"
|
||
|
||
result = server.write_new_file(filename, "unicode")
|
||
|
||
assert "OK:" in result
|
||
assert (workspace / filename).exists()
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 20. Tiefe Verzeichnisstrukturen
|
||
# ---------------------------------------------------------
|
||
|
||
@pytest.mark.skipif(
|
||
sys.platform == "win32",
|
||
reason="Windows MAX_PATH limit (260 chars) prevents deep nesting",
|
||
)
|
||
def test_list_files_deep_nesting(workspace):
|
||
current = workspace
|
||
|
||
for i in range(50):
|
||
current = current / f"dir_{i}"
|
||
current.mkdir()
|
||
|
||
file = current / "deep.txt"
|
||
file.write_text("deep")
|
||
|
||
result = server.list_files()
|
||
|
||
assert "deep.txt" in result |