diff --git a/frontend/sidebar.py b/frontend/sidebar.py index 1a6b9d4..82de7b0 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -240,10 +240,10 @@ def render_filetree_arborist(tree): selected = tree_view( data=data, icons={"open": "📂", "closed": "📁"}, - height=200, + height=400, selection=None, select_internal_nodes=True, # allow clicking folder names, not just files - open_by_default=False + open_by_default=True ) return selected diff --git a/tests/test_mcp_server_file_search.py b/tests/test_mcp_server_file_search.py index e69de29..61481e9 100644 --- a/tests/test_mcp_server_file_search.py +++ b/tests/test_mcp_server_file_search.py @@ -0,0 +1,278 @@ +import pytest +from pathlib import Path +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() + + assert "src/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 +# --------------------------------------------------------- + +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 +# --------------------------------------------------------- + +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 \ No newline at end of file diff --git a/tests/test_mcp_server_web_search.py b/tests/test_mcp_server_web_search.py index e69de29..66835ea 100644 --- a/tests/test_mcp_server_web_search.py +++ b/tests/test_mcp_server_web_search.py @@ -0,0 +1,297 @@ +import pytest +from unittest.mock import Mock, patch +from backend.agent.servers import mcp_server_web_search as server + + +# ========================================================= +# BASIC TESTS (1–10) +# ========================================================= + + +# --------------------------------------------------------- +# 1. _validate_url erlaubt HTTPS +# --------------------------------------------------------- + +def test_validate_url_https(): + url = "https://example.com" + + result = server._validate_url(url) + + assert result == url + + +# --------------------------------------------------------- +# 2. _validate_url erlaubt HTTP +# --------------------------------------------------------- + +def test_validate_url_http(): + url = "http://example.com" + + result = server._validate_url(url) + + assert result == url + + +# --------------------------------------------------------- +# 3. _validate_url blockiert localhost +# --------------------------------------------------------- + +def test_validate_url_localhost(): + with pytest.raises(ValueError): + server._validate_url("http://localhost/admin") + + +# --------------------------------------------------------- +# 4. _validate_url blockiert 127.0.0.1 +# --------------------------------------------------------- + +def test_validate_url_loopback(): + with pytest.raises(ValueError): + server._validate_url("http://127.0.0.1") + + +# --------------------------------------------------------- +# 5. _validate_url blockiert private IP +# --------------------------------------------------------- + +def test_validate_url_private_ip(): + with pytest.raises(ValueError): + server._validate_url("http://192.168.1.10") + + +# --------------------------------------------------------- +# 6. web_search liefert Suchergebnisse +# --------------------------------------------------------- + +@patch("ddgs.DDGS") +def test_web_search_success(mock_ddgs): + mock_instance = Mock() + + mock_instance.text.return_value = [ + { + "title": "Example", + "href": "https://example.com", + "body": "Example snippet" + } + ] + + mock_ddgs.return_value = mock_instance + + result = server.web_search("example") + + assert "Title: Example" in result + assert "https://example.com" in result + + +# --------------------------------------------------------- +# 7. web_search ohne Ergebnisse +# --------------------------------------------------------- + +@patch("ddgs.DDGS") +def test_web_search_no_results(mock_ddgs): + mock_instance = Mock() + mock_instance.text.return_value = [] + + mock_ddgs.return_value = mock_instance + + result = server.web_search("nothing") + + assert "No results found" in result + + +# --------------------------------------------------------- +# 8. fetch_page lädt HTML +# --------------------------------------------------------- + +@patch("requests.get") +def test_fetch_page_success(mock_get): + response = Mock() + + response.status_code = 200 + response.text = """ + +
+