Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

335 lines
13 KiB
Python

"""Tests for PathValidator — security-sensitive path validation edge cases."""
from pathlib import Path
import pytest
from local_deep_research.security.path_validator import PathValidator
# ===========================================================================
# 1. validate_safe_path
# ===========================================================================
class TestValidateSafePath:
"""Verify validate_safe_path input validation and traversal blocking."""
def test_non_string_input_rejected(self):
"""Non-string input raises ValueError."""
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_safe_path(123, "/tmp")
def test_none_input_rejected(self):
"""None input raises ValueError."""
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_safe_path(None, "/tmp")
def test_empty_string_rejected(self):
"""Empty string raises ValueError."""
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_safe_path("", "/tmp")
def test_null_bytes_rejected_in_safe_path(self, tmp_path):
"""Null bytes in validate_safe_path raise ValueError."""
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_safe_path("file\x00.txt", str(tmp_path))
def test_traversal_blocked(self, tmp_path):
"""Path traversal attempt via .. is blocked."""
with pytest.raises(ValueError):
PathValidator.validate_safe_path("../../etc/passwd", str(tmp_path))
def test_valid_relative_path(self, tmp_path):
"""Valid relative path returns resolved Path."""
result = PathValidator.validate_safe_path(
"subdir/file.txt", str(tmp_path)
)
assert result is not None
assert str(tmp_path) in str(result)
def test_extension_check_rejects_wrong_extension(self, tmp_path):
"""Wrong extension raises ValueError."""
with pytest.raises(ValueError, match="Invalid file type"):
PathValidator.validate_safe_path(
"file.txt",
str(tmp_path),
required_extensions=(".json", ".yaml"),
)
def test_extension_check_accepts_correct_extension(self, tmp_path):
"""Correct extension passes validation."""
result = PathValidator.validate_safe_path(
"config.json",
str(tmp_path),
required_extensions=(".json", ".yaml"),
)
assert result.suffix == ".json"
def test_extension_case_sensitivity(self, tmp_path):
"""Extension check is case-sensitive — .JSON != .json."""
with pytest.raises(ValueError, match="Invalid file type"):
PathValidator.validate_safe_path(
"config.JSON",
str(tmp_path),
required_extensions=(".json",),
)
def test_whitespace_stripped(self, tmp_path):
"""Leading/trailing whitespace in input is stripped."""
result = PathValidator.validate_safe_path(" file.txt ", str(tmp_path))
assert result is not None
assert "file.txt" in str(result)
# ===========================================================================
# 2. validate_local_filesystem_path — home dir expansion
# ===========================================================================
class TestHomeDirExpansion:
"""Verify ~ expansion edge cases."""
def test_tilde_slash_expands_to_home(self):
"""'~/' expands to home directory."""
home = str(Path.home())
result = PathValidator.validate_local_filesystem_path(
"~/documents",
restricted_dirs=[],
)
assert str(result).startswith(home)
def test_tilde_alone_expands_to_home(self):
"""'~' alone expands to home directory."""
home = Path.home().resolve()
result = PathValidator.validate_local_filesystem_path(
"~",
restricted_dirs=[],
)
assert result == home
def test_tilde_with_trailing_slashes(self):
"""'~/' with no relative part yields home directory."""
home = Path.home().resolve()
result = PathValidator.validate_local_filesystem_path(
"~/",
restricted_dirs=[],
)
assert result == home
def test_null_bytes_rejected(self):
"""Null bytes in path raise ValueError."""
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_local_filesystem_path("/tmp/file\x00.txt")
def test_control_characters_rejected(self):
"""Control characters raise ValueError."""
with pytest.raises(ValueError, match="Control characters"):
PathValidator.validate_local_filesystem_path("/tmp/file\x01name")
def test_path_traversal_blocked(self):
""".. in path raises ValueError."""
with pytest.raises(ValueError, match="traversal"):
PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd")
def test_restricted_dir_blocked(self):
"""Access to /etc is blocked by default restrictions."""
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path("/etc/passwd")
def test_custom_restricted_dir(self, tmp_path):
"""Custom restricted dirs are enforced."""
restricted = tmp_path / "secret"
restricted.mkdir()
target = restricted / "file.txt"
target.touch()
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path(
str(target),
restricted_dirs=[restricted],
)
# ===========================================================================
# 3. sanitize_for_filesystem_ops — entirely uncovered
# ===========================================================================
class TestSanitizeForFilesystemOps:
"""Verify sanitize_for_filesystem_ops validation."""
def test_relative_path_rejected(self):
"""Relative path raises ValueError."""
with pytest.raises(ValueError, match="must be absolute"):
PathValidator.sanitize_for_filesystem_ops(Path("relative/path"))
def test_absolute_path_passes(self, tmp_path):
"""Absolute path is returned as a Path."""
result = PathValidator.sanitize_for_filesystem_ops(tmp_path)
assert isinstance(result, Path)
assert result.is_absolute()
def test_root_path(self):
"""Root path '/' passes sanitization."""
result = PathValidator.sanitize_for_filesystem_ops(Path("/"))
assert result == Path("/")
def test_deep_path_preserved(self, tmp_path):
"""Deep nested path structure is preserved."""
deep = tmp_path / "a" / "b" / "c"
result = PathValidator.sanitize_for_filesystem_ops(deep)
assert str(result).endswith("a/b/c")
# ===========================================================================
# 4. validate_model_path
# ===========================================================================
class TestValidateModelPath:
"""Verify validate_model_path checks."""
def test_nonexistent_model_file(self, tmp_path):
"""Non-existent model file raises ValueError."""
with pytest.raises(ValueError, match="Model file not found"):
PathValidator.validate_model_path(
"model.bin", model_root=str(tmp_path)
)
def test_directory_not_file_rejected(self, tmp_path):
"""Directory path raises 'not a file' error."""
subdir = tmp_path / "model_dir"
subdir.mkdir()
with pytest.raises(ValueError, match="not a file"):
PathValidator.validate_model_path(
"model_dir", model_root=str(tmp_path)
)
def test_valid_model_file(self, tmp_path):
"""Valid model file returns resolved path."""
model_file = tmp_path / "model.gguf"
model_file.touch()
result = PathValidator.validate_model_path(
"model.gguf", model_root=str(tmp_path)
)
assert result == model_file.resolve()
def test_traversal_in_model_path(self, tmp_path):
"""Path traversal in model path is blocked."""
with pytest.raises(ValueError):
PathValidator.validate_model_path(
"../../../etc/passwd", model_root=str(tmp_path)
)
def test_model_root_created_if_missing(self, tmp_path):
"""Model root directory is created if it doesn't exist."""
new_root = tmp_path / "new_models"
# Will fail because file doesn't exist, but root should be created
try:
PathValidator.validate_model_path(
"test.bin", model_root=str(new_root)
)
except ValueError:
pass
assert new_root.exists()
# ===========================================================================
# 5. validate_config_path — restricted prefixes and null byte rejection
# ===========================================================================
class TestValidateConfigPath:
"""Verify validate_config_path security checks."""
def test_non_string_rejected(self):
"""Non-string input raises ValueError."""
with pytest.raises(ValueError, match="Invalid config path"):
PathValidator.validate_config_path(123)
def test_empty_string_rejected(self):
"""Empty string raises ValueError."""
with pytest.raises(ValueError, match="Invalid config path"):
PathValidator.validate_config_path("")
def test_null_bytes_rejected(self, tmp_path):
"""Null bytes in config paths raise ValueError."""
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_config_path(
"config\x00.json",
config_root=str(tmp_path),
)
def test_traversal_rejected(self):
""".. in config path raises ValueError."""
with pytest.raises(ValueError, match="traversal"):
PathValidator.validate_config_path(
"../etc/passwd", config_root="/tmp"
)
@pytest.mark.parametrize(
"restricted", ["etc/passwd", "proc/self", "sys/class", "dev/null"]
)
def test_restricted_prefixes_blocked(self, restricted):
"""System directory prefixes are blocked."""
with pytest.raises(ValueError, match="restricted system directory"):
PathValidator.validate_config_path(f"/{restricted}")
def test_restricted_prefix_exact_match(self):
"""Exact restricted directory name (no trailing path) is blocked."""
with pytest.raises(ValueError, match="restricted system directory"):
PathValidator.validate_config_path("/etc")
def test_invalid_extension_rejected(self, tmp_path):
"""Non-config extension is rejected."""
txt_file = tmp_path / "file.txt"
txt_file.touch()
with pytest.raises(ValueError, match="Invalid"):
PathValidator.validate_config_path(
"file.txt", config_root=str(tmp_path)
)
def test_valid_config_extensions(self, tmp_path):
"""All valid config extensions are accepted."""
for ext in (".json", ".yaml", ".yml", ".toml", ".ini", ".conf"):
config_file = tmp_path / f"config{ext}"
config_file.write_text("")
result = PathValidator.validate_config_path(
f"config{ext}",
config_root=str(tmp_path),
)
assert result is not None
def test_absolute_config_path_rejected_by_safe_join(self, tmp_path):
"""Absolute config paths fail safe_join validation (safe_join rejects absolute second args)."""
config_file = tmp_path / "app.json"
config_file.write_text("{}")
# safe_join("/", "/tmp/.../app.json") returns None because the path is absolute
with pytest.raises(ValueError, match="Invalid absolute path"):
PathValidator.validate_config_path(str(config_file))
def test_restricted_prefix_case_insensitive(self):
"""Restricted prefix check is case-insensitive."""
with pytest.raises(ValueError, match="restricted system directory"):
PathValidator.validate_config_path("/ETC/passwd")
# ===========================================================================
# 6. validate_data_path
# ===========================================================================
class TestValidateDataPath:
"""Verify validate_data_path basics."""
def test_valid_data_path(self, tmp_path):
"""Valid relative path returns resolved path."""
result = PathValidator.validate_data_path("data.db", str(tmp_path))
assert "data.db" in str(result)
def test_traversal_in_data_path(self, tmp_path):
"""Path traversal is blocked."""
with pytest.raises(ValueError):
PathValidator.validate_data_path("../../etc/passwd", str(tmp_path))