# coding=utf-8 """ @Time :2026/6/30 @Author :tao.chen """ from pathlib import Path import pytest from files_mcp.core import path_guard @pytest.fixture def sandbox_root(tmp_path, monkeypatch): """Fresh tmp_path-based sandbox root for each test. Pre-populates a regular file, a nested file inside a subdir, and a symlink that points outside the sandbox (for the symlink-escape test). """ monkeypatch.setenv("FILES_MCP_ROOT", str(tmp_path)) path_guard.set_root_for_testing(tmp_path.resolve()) (tmp_path / "inside.txt").write_text("hi", encoding="utf-8") (tmp_path / "subdir").mkdir() (tmp_path / "subdir" / "nested.txt").write_text("nested", encoding="utf-8") (tmp_path / "escape_link").symlink_to("/etc/passwd") return tmp_path.resolve() class TestResolveAndCheck: def test_accepts_inside_path(self, sandbox_root): p = path_guard.resolve_and_check("inside.txt") assert p == sandbox_root / "inside.txt" def test_accepts_nested_path(self, sandbox_root): p = path_guard.resolve_and_check("subdir/nested.txt") assert p == sandbox_root / "subdir" / "nested.txt" def test_accepts_dot_segments(self, sandbox_root): p = path_guard.resolve_and_check("subdir/../inside.txt") assert p == sandbox_root / "inside.txt" def test_rejects_double_dot_escape(self, sandbox_root): with pytest.raises(ValueError, match="escapes sandbox"): path_guard.resolve_and_check("../etc/passwd") def test_rejects_absolute_outside_path(self, sandbox_root): with pytest.raises(ValueError, match="escapes sandbox"): path_guard.resolve_and_check("/etc/passwd") def test_rejects_mixed_escape(self, sandbox_root): with pytest.raises(ValueError, match="escapes sandbox"): path_guard.resolve_and_check("subdir/../../etc/passwd") def test_rejects_symlink_escape(self, sandbox_root): # escape_link -> /etc/passwd; resolve() follows the link first. with pytest.raises(ValueError, match="escapes sandbox"): path_guard.resolve_and_check("escape_link") def test_rejects_empty(self, sandbox_root): with pytest.raises(ValueError, match="path is empty"): path_guard.resolve_and_check("") def test_rejects_nul_byte(self, sandbox_root): with pytest.raises(ValueError, match="NUL byte"): path_guard.resolve_and_check("foo\x00bar") class TestInitRoot: def test_init_root_raises_if_unset(self, monkeypatch): monkeypatch.delenv("FILES_MCP_ROOT", raising=False) path_guard._ROOT = None with pytest.raises(RuntimeError, match="FILES_MCP_ROOT"): path_guard._init_root() def test_init_root_raises_if_not_directory(self, tmp_path, monkeypatch): monkeypatch.setenv("FILES_MCP_ROOT", str(tmp_path / "missing")) path_guard._ROOT = None with pytest.raises(RuntimeError, match="not an existing directory"): path_guard._init_root() def test_init_root_is_idempotent(self, sandbox_root): first = path_guard._ROOT path_guard._init_root() assert path_guard._ROOT == first