chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Unit tests for ``lightrag.utils.validate_workspace``.
|
||||
|
||||
File-based storages build a per-workspace subdirectory under ``working_dir``
|
||||
via ``os.path.join(working_dir, workspace)``. ``validate_workspace`` guards that
|
||||
join against path traversal by rejecting any name that is not a single path
|
||||
component, while leaving legitimate names (including dotted ones) untouched.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from lightrag.utils import validate_workspace
|
||||
|
||||
pytestmark = pytest.mark.offline
|
||||
|
||||
|
||||
class TestValidWorkspaces:
|
||||
"""Names that are valid single path components pass through unchanged."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"workspace",
|
||||
[
|
||||
"my_workspace",
|
||||
"workspace-123",
|
||||
"MyWorkSpace",
|
||||
"12345",
|
||||
"_",
|
||||
"v1.0", # dots are safe in a single path component
|
||||
"team.alpha",
|
||||
"a..b", # ".." embedded in a name is not a traversal
|
||||
"工作区_test", # non-ASCII is fine for a directory name
|
||||
],
|
||||
)
|
||||
def test_returns_input_unchanged(self, workspace):
|
||||
assert validate_workspace(workspace) == workspace
|
||||
|
||||
|
||||
class TestPathTraversalRejected:
|
||||
"""Names that could escape ``working_dir`` raise ``ValueError``."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"workspace",
|
||||
[
|
||||
"..",
|
||||
".",
|
||||
"../etc",
|
||||
"../../../etc/passwd",
|
||||
"foo/../../etc",
|
||||
"foo/bar", # nested path, not a single component
|
||||
"/etc/passwd", # absolute path would discard working_dir on join
|
||||
"..\\..\\windows", # Windows-style separator
|
||||
"foo\\bar",
|
||||
],
|
||||
)
|
||||
def test_raises_value_error(self, workspace):
|
||||
with pytest.raises(ValueError):
|
||||
validate_workspace(workspace)
|
||||
|
||||
|
||||
class TestStorageRejectsTraversal:
|
||||
"""The check is wired into storage construction, not just the helper."""
|
||||
|
||||
def test_json_kv_storage_rejects_traversal(self, tmp_path):
|
||||
from lightrag.kg.json_kv_impl import JsonKVStorage
|
||||
|
||||
cfg = {"working_dir": str(tmp_path)}
|
||||
with pytest.raises(ValueError):
|
||||
JsonKVStorage(
|
||||
namespace="ns",
|
||||
workspace="../../../etc",
|
||||
global_config=cfg,
|
||||
embedding_func=None,
|
||||
)
|
||||
|
||||
def test_json_kv_storage_accepts_dotted_name(self, tmp_path):
|
||||
from lightrag.kg.json_kv_impl import JsonKVStorage
|
||||
|
||||
cfg = {"working_dir": str(tmp_path)}
|
||||
storage = JsonKVStorage(
|
||||
namespace="ns",
|
||||
workspace="v1.0",
|
||||
global_config=cfg,
|
||||
embedding_func=None,
|
||||
)
|
||||
assert storage.workspace == "v1.0"
|
||||
|
||||
|
||||
def _import_document_manager():
|
||||
"""Import DocumentManager with a clean argv.
|
||||
|
||||
Importing ``lightrag.api`` modules triggers the server's argparse against
|
||||
``sys.argv`` at import time, which would otherwise see pytest's arguments
|
||||
(same workaround as tests/api/test_path_prefixes.py).
|
||||
"""
|
||||
import sys
|
||||
|
||||
original_argv = sys.argv.copy()
|
||||
try:
|
||||
sys.argv = ["lightrag-server"]
|
||||
from lightrag.api.routers.document_routes import DocumentManager
|
||||
|
||||
return DocumentManager
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
class TestUploadPath:
|
||||
"""DocumentManager builds the upload dir from the workspace; the same
|
||||
validation must guard it, and the API server's own sanitizer
|
||||
(``re.sub(r"[^a-zA-Z0-9_]", "_", ...)`` in api/config.py) must never
|
||||
produce a value our validator rejects."""
|
||||
|
||||
def test_document_manager_rejects_traversal(self, tmp_path):
|
||||
DocumentManager = _import_document_manager()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
DocumentManager(str(tmp_path), workspace="../../outside")
|
||||
assert not (tmp_path.parent.parent / "outside").exists()
|
||||
|
||||
def test_document_manager_creates_workspace_subdir(self, tmp_path):
|
||||
DocumentManager = _import_document_manager()
|
||||
|
||||
dm = DocumentManager(str(tmp_path), workspace="space1")
|
||||
assert dm.input_dir == tmp_path / "space1"
|
||||
assert dm.input_dir.is_dir()
|
||||
|
||||
def test_document_manager_empty_workspace_uses_base_dir(self, tmp_path):
|
||||
DocumentManager = _import_document_manager()
|
||||
|
||||
dm = DocumentManager(str(tmp_path), workspace="")
|
||||
assert dm.input_dir == tmp_path
|
||||
|
||||
def test_api_sanitizer_output_always_accepted(self):
|
||||
import re
|
||||
|
||||
for raw in [
|
||||
"my workspace",
|
||||
"v1.0",
|
||||
"../../../etc",
|
||||
"a/b",
|
||||
"..\\win",
|
||||
"工作区",
|
||||
"..",
|
||||
".",
|
||||
]:
|
||||
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", raw)
|
||||
assert validate_workspace(sanitized) == sanitized
|
||||
|
||||
def test_upload_dir_matches_storage_dir(self, tmp_path):
|
||||
"""Files uploaded to <input_dir>/<ws>/ must be resolvable against
|
||||
storage data in <working_dir>/<ws>/ — same subdirectory name."""
|
||||
DocumentManager = _import_document_manager()
|
||||
from lightrag.kg.json_kv_impl import JsonKVStorage
|
||||
|
||||
inputs = tmp_path / "inputs"
|
||||
working = tmp_path / "rag_storage"
|
||||
inputs.mkdir()
|
||||
working.mkdir()
|
||||
|
||||
dm = DocumentManager(str(inputs), workspace="space1")
|
||||
kv = JsonKVStorage(
|
||||
namespace="ns",
|
||||
workspace="space1",
|
||||
global_config={"working_dir": str(working)},
|
||||
embedding_func=None,
|
||||
)
|
||||
upload_subdir = dm.input_dir.relative_to(inputs)
|
||||
storage_subdir = Path(kv._file_name).parent.relative_to(working)
|
||||
assert upload_subdir == storage_subdir
|
||||
Reference in New Issue
Block a user