initial commit after one day coding agent session
This commit is contained in:
commit
372af75b90
88 changed files with 22694 additions and 0 deletions
271
tests/builder/test_package_validator.py
Normal file
271
tests/builder/test_package_validator.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
"""Tests for Package Validator module."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mcp_forge.builder.package_validator import (
|
||||
PackageValidator,
|
||||
ApprovalRequiredError,
|
||||
SecurityError
|
||||
)
|
||||
from mcp_forge.config.schema import PackageValidationConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def allowlist_file(tmp_path):
|
||||
"""Create temporary allowlist file."""
|
||||
allowlist = tmp_path / "allowlist.txt"
|
||||
allowlist.write_text("""
|
||||
# Standard data science packages
|
||||
numpy
|
||||
pandas
|
||||
scipy
|
||||
scikit-learn
|
||||
matplotlib
|
||||
|
||||
# Web and API
|
||||
requests
|
||||
httpx
|
||||
aiohttp
|
||||
|
||||
# Utilities
|
||||
pyyaml
|
||||
python-dateutil
|
||||
""".strip())
|
||||
return allowlist
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blocklist_file(tmp_path):
|
||||
"""Create temporary blocklist file."""
|
||||
blocklist = tmp_path / "blocklist.txt"
|
||||
blocklist.write_text("""
|
||||
# Security concerns
|
||||
os-crypto
|
||||
subprocess-wrapper
|
||||
shell-exec
|
||||
|
||||
# Known malicious
|
||||
malicious-package
|
||||
evil-lib
|
||||
""".strip())
|
||||
return blocklist
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validation_config(allowlist_file, blocklist_file, tmp_path):
|
||||
"""Mock PackageValidationConfig."""
|
||||
config = Mock(spec=PackageValidationConfig)
|
||||
config.use_allowlist = True
|
||||
config.allowlist_path = allowlist_file
|
||||
config.blocklist_path = blocklist_file
|
||||
config.require_approval_patterns = [
|
||||
"^torch.*", # PyTorch packages
|
||||
"^tensorflow.*", # TensorFlow packages
|
||||
".*-gpu$", # GPU variants
|
||||
]
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validator(validation_config):
|
||||
"""PackageValidator instance."""
|
||||
return PackageValidator(validation_config)
|
||||
|
||||
|
||||
def test_extract_package_name_simple(validator):
|
||||
"""Test extracting package name from simple spec."""
|
||||
assert validator.extract_package_name("numpy") == "numpy"
|
||||
assert validator.extract_package_name("pandas") == "pandas"
|
||||
|
||||
|
||||
def test_extract_package_name_with_version(validator):
|
||||
"""Test extracting package name with version specifiers."""
|
||||
assert validator.extract_package_name("numpy>=1.24.0") == "numpy"
|
||||
assert validator.extract_package_name("pandas==2.0.0") == "pandas"
|
||||
assert validator.extract_package_name("requests<=2.28.0") == "requests"
|
||||
assert validator.extract_package_name("scikit-learn~=1.3.0") == "scikit-learn"
|
||||
|
||||
|
||||
def test_extract_package_name_with_extras(validator):
|
||||
"""Test extracting package name with extras."""
|
||||
assert validator.extract_package_name("requests[security]") == "requests"
|
||||
assert validator.extract_package_name("pandas[excel,sql]") == "pandas"
|
||||
|
||||
|
||||
def test_extract_package_name_complex(validator):
|
||||
"""Test extracting package name from complex specs."""
|
||||
assert validator.extract_package_name("numpy>=1.24.0,<2.0.0") == "numpy"
|
||||
assert validator.extract_package_name("requests[security]>=2.28.0") == "requests"
|
||||
|
||||
|
||||
def test_allowlisted_package_passes(validator):
|
||||
"""Test that allowlisted packages pass validation."""
|
||||
validator.validate_package("numpy")
|
||||
validator.validate_package("pandas>=2.0.0")
|
||||
validator.validate_package("requests[security]")
|
||||
# Should not raise
|
||||
|
||||
|
||||
def test_blocklisted_package_raises_error(validator):
|
||||
"""Test that blocklisted packages raise SecurityError."""
|
||||
with pytest.raises(SecurityError, match="malicious-package"):
|
||||
validator.validate_package("malicious-package")
|
||||
|
||||
with pytest.raises(SecurityError, match="evil-lib"):
|
||||
validator.validate_package("evil-lib>=1.0.0")
|
||||
|
||||
|
||||
def test_unknown_package_with_allowlist_raises_error(validator):
|
||||
"""Test that unknown packages raise error when allowlist is enabled."""
|
||||
with pytest.raises(SecurityError, match="unknown-package"):
|
||||
validator.validate_package("unknown-package")
|
||||
|
||||
|
||||
def test_package_requiring_approval_raises_error(validator):
|
||||
"""Test that packages matching approval patterns raise ApprovalRequiredError."""
|
||||
with pytest.raises(ApprovalRequiredError, match="torch"):
|
||||
validator.validate_package("torch")
|
||||
|
||||
with pytest.raises(ApprovalRequiredError, match="tensorflow"):
|
||||
validator.validate_package("tensorflow-gpu")
|
||||
|
||||
with pytest.raises(ApprovalRequiredError, match="gpu"):
|
||||
validator.validate_package("cupy-gpu")
|
||||
|
||||
|
||||
def test_validate_packages_list(validator):
|
||||
"""Test validating multiple packages at once."""
|
||||
packages = ["numpy>=1.24.0", "pandas", "requests"]
|
||||
validator.validate_packages(packages)
|
||||
# Should not raise
|
||||
|
||||
|
||||
def test_validate_packages_enforces_max_limit(validator):
|
||||
"""Test that validate_packages enforces maximum package count."""
|
||||
packages = ["numpy", "pandas", "scipy", "matplotlib"]
|
||||
|
||||
with pytest.raises(ValueError, match="Maximum 3 packages"):
|
||||
validator.validate_packages(packages, max_packages=3)
|
||||
|
||||
|
||||
def test_validate_packages_with_mixed_results(validator):
|
||||
"""Test that validation stops at first error."""
|
||||
packages = ["numpy", "malicious-package", "pandas"]
|
||||
|
||||
with pytest.raises(SecurityError, match="malicious-package"):
|
||||
validator.validate_packages(packages)
|
||||
|
||||
|
||||
def test_validate_packages_with_approval_required(validator):
|
||||
"""Test that validation stops at first approval requirement."""
|
||||
packages = ["numpy", "torch", "pandas"]
|
||||
|
||||
with pytest.raises(ApprovalRequiredError, match="torch"):
|
||||
validator.validate_packages(packages)
|
||||
|
||||
|
||||
def test_allowlist_loading(allowlist_file):
|
||||
"""Test that allowlist is loaded correctly from file."""
|
||||
config = Mock(spec=PackageValidationConfig)
|
||||
config.use_allowlist = True
|
||||
config.allowlist_path = allowlist_file
|
||||
config.blocklist_path = None
|
||||
config.require_approval_patterns = []
|
||||
|
||||
validator = PackageValidator(config)
|
||||
|
||||
assert "numpy" in validator.allowlist
|
||||
assert "pandas" in validator.allowlist
|
||||
assert "requests" in validator.allowlist
|
||||
# Comments and empty lines should be ignored
|
||||
assert "# Standard data science packages" not in validator.allowlist
|
||||
|
||||
|
||||
def test_blocklist_loading(blocklist_file):
|
||||
"""Test that blocklist is loaded correctly from file."""
|
||||
config = Mock(spec=PackageValidationConfig)
|
||||
config.use_allowlist = False
|
||||
config.allowlist_path = None
|
||||
config.blocklist_path = blocklist_file
|
||||
config.require_approval_patterns = []
|
||||
|
||||
validator = PackageValidator(config)
|
||||
|
||||
assert "malicious-package" in validator.blocklist
|
||||
assert "evil-lib" in validator.blocklist
|
||||
|
||||
|
||||
def test_disabled_allowlist_allows_all_except_blocklist(blocklist_file):
|
||||
"""Test that disabling allowlist allows any package except blocklisted."""
|
||||
config = Mock(spec=PackageValidationConfig)
|
||||
config.use_allowlist = False
|
||||
config.allowlist_path = None
|
||||
config.blocklist_path = blocklist_file
|
||||
config.require_approval_patterns = []
|
||||
|
||||
validator = PackageValidator(config)
|
||||
|
||||
# Unknown packages should pass
|
||||
validator.validate_package("some-random-package")
|
||||
|
||||
# But blocklisted packages should still fail
|
||||
with pytest.raises(SecurityError, match="malicious-package"):
|
||||
validator.validate_package("malicious-package")
|
||||
|
||||
|
||||
def test_approval_pattern_matching(validator):
|
||||
"""Test that approval patterns match correctly."""
|
||||
# torch* should match
|
||||
with pytest.raises(ApprovalRequiredError):
|
||||
validator.validate_package("torch")
|
||||
|
||||
with pytest.raises(ApprovalRequiredError):
|
||||
validator.validate_package("torchvision")
|
||||
|
||||
# *-gpu$ should match
|
||||
with pytest.raises(ApprovalRequiredError):
|
||||
validator.validate_package("something-gpu")
|
||||
|
||||
|
||||
def test_empty_package_list(validator):
|
||||
"""Test validating empty package list."""
|
||||
validator.validate_packages([])
|
||||
# Should not raise
|
||||
|
||||
|
||||
def test_validate_packages_without_max_limit(validator):
|
||||
"""Test validating many packages without limit."""
|
||||
packages = [f"package{i}" for i in range(100)]
|
||||
|
||||
# Should raise because packages aren't in allowlist
|
||||
with pytest.raises(SecurityError):
|
||||
validator.validate_packages(packages)
|
||||
|
||||
|
||||
def test_case_sensitivity(validator):
|
||||
"""Test that package names are case-sensitive."""
|
||||
# numpy is in allowlist
|
||||
validator.validate_package("numpy")
|
||||
|
||||
# NumPy (different case) should fail
|
||||
with pytest.raises(SecurityError, match="NumPy"):
|
||||
validator.validate_package("NumPy")
|
||||
|
||||
|
||||
def test_whitespace_handling(validator):
|
||||
"""Test that leading/trailing whitespace is handled."""
|
||||
validator.validate_package(" numpy ")
|
||||
validator.validate_package(" pandas>=2.0.0 ")
|
||||
|
||||
|
||||
def test_file_not_found_handling(tmp_path):
|
||||
"""Test handling of missing allowlist/blocklist files."""
|
||||
config = Mock(spec=PackageValidationConfig)
|
||||
config.use_allowlist = True
|
||||
config.allowlist_path = tmp_path / "nonexistent.txt"
|
||||
config.blocklist_path = None
|
||||
config.require_approval_patterns = []
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
PackageValidator(config)
|
||||
Loading…
Add table
Add a link
Reference in a new issue