mcilspy/tests/test_error_paths.py
Ryan Malloy 3c21b9d640 test: fix test compatibility with security and architecture changes
- Add bypass_path_validation fixture for tests using mock paths
- Update find_ilspycmd_path references to use mcilspy.utils
- Fix wrapper fixture patches in timeout tests
- Update assertions for new output formats (pagination, etc.)
- Mark all taskmaster domains as merged in status.json

All 165 tests passing.
2026-02-08 11:47:14 -07:00

426 lines
15 KiB
Python

"""Tests for error handling paths.
These tests verify that the server handles various error conditions gracefully:
- Invalid regex patterns
- ilspycmd not found scenarios
- Invalid language versions
- File not found errors
- Invalid assembly files
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mcilspy import server
from mcilspy.ilspy_wrapper import ILSpyWrapper
from mcilspy.metadata_reader import MetadataReader
from mcilspy.models import EntityType
# Fixture to bypass path validation for tests using mock paths
@pytest.fixture
def bypass_path_validation():
"""Bypass _validate_assembly_path for tests using mock wrapper."""
def passthrough(path):
return path
with patch.object(server, "_validate_assembly_path", side_effect=passthrough):
yield
class TestInvalidRegexPatterns:
"""Tests for invalid regex pattern handling."""
@pytest.mark.asyncio
async def test_search_types_invalid_regex(self, test_assembly_path):
"""Test search_types with invalid regex pattern."""
# Use an invalid regex pattern
result = await server.search_types(
test_assembly_path,
pattern="[invalid(regex",
use_regex=True,
)
assert "Invalid regex pattern" in result
@pytest.mark.asyncio
async def test_search_methods_invalid_regex(self, test_assembly_path):
"""Test search_methods with invalid regex pattern."""
result = await server.search_methods(
test_assembly_path,
pattern="[unclosed",
use_regex=True,
)
assert "Invalid regex pattern" in result
@pytest.mark.asyncio
async def test_search_fields_invalid_regex(self, test_assembly_path):
"""Test search_fields with invalid regex pattern."""
result = await server.search_fields(
test_assembly_path,
pattern="*invalid*",
use_regex=True,
)
assert "Invalid regex pattern" in result
@pytest.mark.asyncio
async def test_search_properties_invalid_regex(self, test_assembly_path):
"""Test search_properties with invalid regex pattern."""
result = await server.search_properties(
test_assembly_path,
pattern="(?P<broken",
use_regex=True,
)
assert "Invalid regex pattern" in result
@pytest.mark.asyncio
async def test_search_strings_invalid_regex(self, test_assembly_path):
"""Test search_strings with invalid regex pattern."""
# Now uses fast MetadataReader search - no wrapper needed
result = await server.search_strings(
test_assembly_path,
pattern="[broken",
use_regex=True,
)
assert "Invalid regex pattern" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestIlspyCmdNotFound:
"""Tests for scenarios where ilspycmd is not installed."""
def test_wrapper_init_raises_when_not_found(self):
"""Test that ILSpyWrapper raises RuntimeError when ilspycmd not found."""
with (
patch("shutil.which", return_value=None),
patch("os.path.isfile", return_value=False),
pytest.raises(RuntimeError) as exc_info,
):
ILSpyWrapper()
assert "ILSpyCmd not found" in str(exc_info.value)
@pytest.mark.asyncio
async def test_decompile_when_not_installed(self):
"""Test decompile_assembly when ilspycmd is not installed."""
with patch.object(
server, "get_wrapper", side_effect=RuntimeError("ILSpyCmd not found")
):
result = await server.decompile_assembly("/path/to/test.dll")
assert "Error" in result
assert "ILSpyCmd not found" in result
@pytest.mark.asyncio
async def test_list_types_when_not_installed(self):
"""Test list_types when ilspycmd is not installed."""
with patch.object(
server, "get_wrapper", side_effect=RuntimeError("ILSpyCmd not found")
):
result = await server.list_types("/path/to/test.dll")
assert "Error" in result
assert "ILSpyCmd not found" in result
@pytest.mark.asyncio
async def test_generate_diagrammer_when_not_installed(self):
"""Test generate_diagrammer when ilspycmd is not installed."""
with patch.object(
server, "get_wrapper", side_effect=RuntimeError("ILSpyCmd not found")
):
result = await server.generate_diagrammer("/path/to/test.dll")
assert "Error" in result
@pytest.mark.asyncio
async def test_get_assembly_info_when_not_installed(self):
"""Test get_assembly_info when ilspycmd is not installed."""
with patch.object(
server, "get_wrapper", side_effect=RuntimeError("ILSpyCmd not found")
):
result = await server.get_assembly_info("/path/to/test.dll")
assert "Error" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestInvalidLanguageVersion:
"""Tests for invalid language version handling."""
@pytest.mark.asyncio
async def test_decompile_with_invalid_language_version(self):
"""Test decompile_assembly with invalid language version."""
# The LanguageVersion enum should raise ValueError for invalid versions
result = await server.decompile_assembly(
"/path/to/test.dll",
language_version="CSharp99", # Invalid version
)
# Should return an error about the invalid language version
assert "Invalid language version" in result or "Error" in result
class TestFileNotFoundErrors:
"""Tests for file not found error handling."""
@pytest.mark.asyncio
async def test_search_methods_file_not_found(self, nonexistent_path):
"""Test search_methods with nonexistent file."""
result = await server.search_methods(nonexistent_path, pattern="test")
assert "Error" in result
@pytest.mark.asyncio
async def test_search_fields_file_not_found(self, nonexistent_path):
"""Test search_fields with nonexistent file."""
result = await server.search_fields(nonexistent_path, pattern="test")
assert "Error" in result
@pytest.mark.asyncio
async def test_search_properties_file_not_found(self, nonexistent_path):
"""Test search_properties with nonexistent file."""
result = await server.search_properties(nonexistent_path, pattern="test")
assert "Error" in result
@pytest.mark.asyncio
async def test_list_events_file_not_found(self, nonexistent_path):
"""Test list_events with nonexistent file."""
result = await server.list_events(nonexistent_path)
assert "Error" in result
@pytest.mark.asyncio
async def test_list_resources_file_not_found(self, nonexistent_path):
"""Test list_resources with nonexistent file."""
result = await server.list_resources(nonexistent_path)
assert "Error" in result
@pytest.mark.asyncio
async def test_get_metadata_summary_file_not_found(self, nonexistent_path):
"""Test get_metadata_summary with nonexistent file."""
result = await server.get_metadata_summary(nonexistent_path)
assert "Error" in result
def test_metadata_reader_file_not_found(self, nonexistent_path):
"""Test MetadataReader with nonexistent file."""
with pytest.raises(FileNotFoundError):
MetadataReader(nonexistent_path)
class TestInvalidAssemblyFiles:
"""Tests for handling invalid assembly files."""
@pytest.fixture
def invalid_assembly_path(self, tmp_path):
"""Create a file that is not a valid .NET assembly."""
invalid_file = tmp_path / "invalid.dll"
invalid_file.write_text("This is not a valid PE file")
return str(invalid_file)
def test_metadata_reader_invalid_assembly(self, invalid_assembly_path):
"""Test MetadataReader with an invalid assembly file."""
# dnfile may silently fail or raise on invalid assemblies
# Either outcome is acceptable - the key is it doesn't crash
try:
with MetadataReader(invalid_assembly_path) as reader:
# If it opens, trying to read should fail or return empty
reader.get_assembly_metadata()
# If we get here, that's OK - just shouldn't crash
assert True
except Exception:
# An exception is also acceptable for invalid PE files
assert True
@pytest.mark.asyncio
async def test_search_methods_invalid_assembly(self, invalid_assembly_path):
"""Test search_methods with invalid assembly."""
result = await server.search_methods(invalid_assembly_path, pattern="test")
assert "Error" in result
@pytest.mark.asyncio
async def test_get_metadata_summary_invalid_assembly(self, invalid_assembly_path):
"""Test get_metadata_summary with invalid assembly."""
result = await server.get_metadata_summary(invalid_assembly_path)
assert "Error" in result
class TestEntityTypeValidation:
"""Tests for EntityType enum validation."""
def test_invalid_entity_type_string(self):
"""Test EntityType.from_string with invalid type name."""
with pytest.raises(ValueError) as exc_info:
EntityType.from_string("invalid_type")
assert "Invalid entity type" in str(exc_info.value)
def test_invalid_entity_type_single_letter(self):
"""Test EntityType.from_string with invalid single letter."""
with pytest.raises(ValueError) as exc_info:
EntityType.from_string("x")
assert "Invalid entity type" in str(exc_info.value)
@pytest.mark.asyncio
async def test_list_types_with_invalid_entity_type(self):
"""Test list_types with invalid entity type in list."""
# The server should skip invalid entity types with a warning
from mcilspy.models import ListTypesResponse
mock_response = ListTypesResponse(success=True, types=[], total_count=0)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
# Should not raise, but should skip invalid types
result = await server.list_types(
"/path/to/test.dll",
entity_types=["class", "invalid", "interface"],
)
# Should still work, just skipping the invalid type
assert isinstance(result, str)
@pytest.mark.usefixtures("bypass_path_validation")
class TestContextInfoFailure:
"""Tests for handling ctx.info() failures."""
@pytest.mark.asyncio
async def test_decompile_with_failing_context(self):
"""Test decompile_assembly when ctx.info() fails."""
from mcilspy.models import DecompileResponse
mock_response = DecompileResponse(
success=True,
assembly_name="Test",
source_code="class Test { }",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
# Create a mock context that fails on info()
mock_ctx = MagicMock()
mock_ctx.info = AsyncMock(side_effect=Exception("Context info failed"))
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
# The function should handle the failing ctx.info gracefully
# Note: Currently ctx is optional and None by default
result = await server.decompile_assembly("/path/to/test.dll", ctx=None)
# Should still succeed since ctx is optional
assert "Test" in result
class TestEmptyResults:
"""Tests for handling empty result sets."""
@pytest.mark.asyncio
async def test_search_methods_empty_assembly(self, test_assembly_path):
"""Test search with pattern that matches nothing."""
result = await server.search_methods(
test_assembly_path, pattern="ZZZZNONEXISTENT"
)
assert "No methods found" in result
@pytest.mark.asyncio
async def test_search_fields_no_matches(self, test_assembly_path):
"""Test field search with no matches."""
result = await server.search_fields(
test_assembly_path, pattern="NONEXISTENT_FIELD_12345"
)
assert "No fields found" in result
@pytest.mark.asyncio
async def test_search_properties_no_matches(self, test_assembly_path):
"""Test property search with no matches."""
result = await server.search_properties(
test_assembly_path, pattern="NONEXISTENT_PROPERTY"
)
assert "No properties found" in result
class TestInstallIlspy:
"""Tests for install_ilspy tool error paths."""
@pytest.mark.asyncio
async def test_install_without_dotnet(self):
"""Test install_ilspy when dotnet is not available."""
mock_status = {
"dotnet_available": False,
"dotnet_version": None,
"ilspycmd_available": False,
"ilspycmd_version": None,
"ilspycmd_path": None,
}
with (
patch.object(server, "_check_dotnet_tools", return_value=mock_status),
patch.object(
server,
"_detect_platform",
return_value={
"system": "linux",
"distro": "arch",
"package_manager": "pacman",
"install_command": "sudo pacman -S dotnet-sdk",
},
),
):
result = await server.install_ilspy()
assert "dotnet CLI is not installed" in result
@pytest.mark.asyncio
async def test_install_already_installed(self):
"""Test install_ilspy when already installed."""
mock_status = {
"dotnet_available": True,
"dotnet_version": "8.0.100",
"ilspycmd_available": True,
"ilspycmd_version": "8.2.0",
"ilspycmd_path": "/home/user/.dotnet/tools/ilspycmd",
}
with patch.object(server, "_check_dotnet_tools", return_value=mock_status):
result = await server.install_ilspy()
assert "already installed" in result
@pytest.mark.asyncio
async def test_install_fails(self):
"""Test install_ilspy when installation fails."""
mock_status_before = {
"dotnet_available": True,
"dotnet_version": "8.0.100",
"ilspycmd_available": False,
"ilspycmd_version": None,
"ilspycmd_path": None,
}
# Mock subprocess to simulate installation failure
mock_proc = MagicMock()
mock_proc.returncode = 1
mock_proc.communicate = AsyncMock(return_value=(b"", b"Installation failed"))
with (
patch.object(server, "_check_dotnet_tools", return_value=mock_status_before),
patch("asyncio.create_subprocess_exec", return_value=mock_proc),
):
result = await server.install_ilspy()
assert "Installation failed" in result or "failed" in result.lower()