"""
Test suite for MCPMixin architecture
Demonstrates how to test modular MCP servers with auto-discovery and validation.
"""
import pytest
import asyncio
from pathlib import Path
from unittest.mock import Mock, AsyncMock
import tempfile
from fastmcp import FastMCP
from mcp_pdf.mixins import (
MCPMixin,
TextExtractionMixin,
TableExtractionMixin,
DocumentAnalysisMixin,
ImageProcessingMixin,
FormManagementMixin,
DocumentAssemblyMixin,
AnnotationsMixin,
)
class TestMCPMixinArchitecture:
"""Test the MCPMixin base architecture and auto-registration"""
def setup_method(self):
"""Setup test environment"""
self.mcp = FastMCP("test-pdf-tools")
self.test_pdf_path = "/tmp/test.pdf"
def test_mixin_auto_registration(self):
"""Test that mixins auto-register their tools"""
# Initialize a mixin
text_mixin = TextExtractionMixin(self.mcp)
# Check that tools were registered
components = text_mixin.get_registered_components()
assert components["mixin"] == "TextExtraction"
assert len(components["tools"]) > 0
assert "extract_text" in components["tools"]
assert "ocr_pdf" in components["tools"]
def test_mixin_permissions(self):
"""Test permission system"""
text_mixin = TextExtractionMixin(self.mcp)
permissions = text_mixin.get_required_permissions()
assert "read_files" in permissions
assert "ocr_processing" in permissions
def test_all_mixins_initialize(self):
"""Test that all mixins can be initialized"""
mixin_classes = [
TextExtractionMixin,
TableExtractionMixin,
DocumentAnalysisMixin,
ImageProcessingMixin,
FormManagementMixin,
DocumentAssemblyMixin,
AnnotationsMixin,
]
for mixin_class in mixin_classes:
mixin = mixin_class(self.mcp)
assert mixin.get_mixin_name()
assert isinstance(mixin.get_required_permissions(), list)
def test_mixin_tool_discovery(self):
"""Test automatic tool discovery from mixin methods"""
text_mixin = TextExtractionMixin(self.mcp)
# Check that public async methods are discovered
components = text_mixin.get_registered_components()
tools = components["tools"]
# Should include methods marked with @mcp_tool
expected_tools = ["extract_text", "ocr_pdf", "is_scanned_pdf"]
for tool in expected_tools:
assert tool in tools, f"Tool {tool} not found in registered tools: {tools}"
class TestTextExtractionMixin:
"""Test the TextExtractionMixin specifically"""
def setup_method(self):
"""Setup test environment"""
self.mcp = FastMCP("test-text-extraction")
self.mixin = TextExtractionMixin(self.mcp)
@pytest.mark.asyncio
async def test_extract_text_validation(self):
"""Test input validation for extract_text"""
# Test empty path
result = await self.mixin.extract_text("")
assert not result["success"]
assert "cannot be empty" in result["error"]
# Test invalid path
result = await self.mixin.extract_text("/nonexistent/file.pdf")
assert not result["success"]
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_is_scanned_pdf_validation(self):
"""Test input validation for is_scanned_pdf"""
result = await self.mixin.is_scanned_pdf("")
assert not result["success"]
assert "cannot be empty" in result["error"]
class TestTableExtractionMixin:
"""Test the TableExtractionMixin specifically"""
def setup_method(self):
"""Setup test environment"""
self.mcp = FastMCP("test-table-extraction")
self.mixin = TableExtractionMixin(self.mcp)
@pytest.mark.asyncio
async def test_extract_tables_fallback_logic(self):
"""Test fallback logic when multiple methods are attempted"""
# This would test the actual fallback mechanism
# For now, just test that the method exists and handles errors
result = await self.mixin.extract_tables("/nonexistent/file.pdf")
assert not result["success"]
assert "fallback_attempts" in result or "error" in result
class TestMixinComposition:
"""Test how mixins work together in a composed server"""
def setup_method(self):
"""Setup test environment"""
self.mcp = FastMCP("test-composed-server")
self.mixins = []
# Initialize all mixins
mixin_classes = [
TextExtractionMixin,
TableExtractionMixin,
DocumentAnalysisMixin,
ImageProcessingMixin,
FormManagementMixin,
DocumentAssemblyMixin,
AnnotationsMixin,
]
for mixin_class in mixin_classes:
mixin = mixin_class(self.mcp)
self.mixins.append(mixin)
def test_no_tool_name_conflicts(self):
"""Test that mixins don't have conflicting tool names"""
all_tools = set()
conflicts = []
for mixin in self.mixins:
components = mixin.get_registered_components()
tools = components["tools"]
for tool in tools:
if tool in all_tools:
conflicts.append(f"Tool '{tool}' registered by multiple mixins")
all_tools.add(tool)
assert not conflicts, f"Tool name conflicts found: {conflicts}"
def test_comprehensive_tool_coverage(self):
"""Test that we have comprehensive tool coverage"""
all_tools = set()
for mixin in self.mixins:
components = mixin.get_registered_components()
all_tools.update(components["tools"])
# Should have a reasonable number of tools (originally had 24+)
assert len(all_tools) >= 15, f"Expected at least 15 tools, got {len(all_tools)}: {sorted(all_tools)}"
# Check for key tool categories
text_tools = [t for t in all_tools if "text" in t or "ocr" in t]
table_tools = [t for t in all_tools if "table" in t]
form_tools = [t for t in all_tools if "form" in t]
assert len(text_tools) > 0, "No text extraction tools found"
assert len(table_tools) > 0, "No table extraction tools found"
assert len(form_tools) > 0, "No form processing tools found"
def test_mixin_permission_aggregation(self):
"""Test that permissions from all mixins can be aggregated"""
all_permissions = set()
for mixin in self.mixins:
permissions = mixin.get_required_permissions()
all_permissions.update(permissions)
# Should include key permission categories
expected_permissions = ["read_files", "write_files"]
for perm in expected_permissions:
assert perm in all_permissions, f"Permission '{perm}' not found in {all_permissions}"
class TestMixinErrorHandling:
"""Test error handling across mixins"""
def setup_method(self):
"""Setup test environment"""
self.mcp = FastMCP("test-error-handling")
def test_mixin_initialization_errors(self):
"""Test how mixins handle initialization errors"""
# Test with invalid configuration
try:
mixin = TextExtractionMixin(self.mcp, invalid_config="test")
# Should still initialize but might log warnings
assert mixin.get_mixin_name() == "TextExtraction"
except Exception as e:
pytest.fail(f"Mixin should handle invalid config gracefully: {e}")
@pytest.mark.asyncio
async def test_tool_error_consistency(self):
"""Test that all tools handle errors consistently"""
text_mixin = TextExtractionMixin(self.mcp)
# All tools should return consistent error format
result = await text_mixin.extract_text("/invalid/path.pdf")
assert isinstance(result, dict)
assert "success" in result
assert result["success"] is False
assert "error" in result
assert isinstance(result["error"], str)
class TestMixinPerformance:
"""Test performance aspects of mixin architecture"""
def test_mixin_initialization_speed(self):
"""Test that mixin initialization is reasonably fast"""
import time
start_time = time.time()
mcp = FastMCP("test-performance")
# Initialize all mixins
mixins = []
mixin_classes = [
TextExtractionMixin,
TableExtractionMixin,
DocumentAnalysisMixin,
ImageProcessingMixin,
FormManagementMixin,
DocumentAssemblyMixin,
AnnotationsMixin,
]
for mixin_class in mixin_classes:
mixin = mixin_class(mcp)
mixins.append(mixin)
initialization_time = time.time() - start_time
# Should initialize in a reasonable time (< 1 second)
assert initialization_time < 1.0, f"Mixin initialization took too long: {initialization_time}s"
def test_tool_registration_efficiency(self):
"""Test that tool registration is efficient"""
mcp = FastMCP("test-registration")
# Time the registration process
import time
start_time = time.time()
text_mixin = TextExtractionMixin(mcp)
registration_time = time.time() - start_time
# Should register quickly
assert registration_time < 0.5, f"Tool registration took too long: {registration_time}s"
if __name__ == "__main__":
pytest.main([__file__, "-v"])