- Use app.run_stdio_async() instead of deprecated stdio_server import - Aligns with FastMCP 2.11.3 API - Server now starts correctly with uv run mcp-office-tools - Maintains all MCPMixin functionality and tool registration
292 lines
8.7 KiB
Python
292 lines
8.7 KiB
Python
"""Pytest configuration and shared fixtures for MCP Office Tools tests.
|
|
|
|
This file provides shared fixtures and configuration for all test modules,
|
|
following FastMCP testing best practices.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, AsyncMock
|
|
from typing import Dict, Any
|
|
|
|
from fastmcp import FastMCP
|
|
# FastMCP testing utilities are created manually
|
|
|
|
from mcp_office_tools.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create a temporary directory for test files."""
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
yield Path(tmp_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_csv_content():
|
|
"""Standard CSV content for testing."""
|
|
return "Name,Age,City,Department\nJohn Doe,30,New York,Engineering\nJane Smith,25,Boston,Marketing\nBob Johnson,35,Chicago,Sales"
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_csv_file(temp_dir, mock_csv_content):
|
|
"""Create a temporary CSV file with test content."""
|
|
csv_file = temp_dir / "test.csv"
|
|
csv_file.write_text(mock_csv_content)
|
|
return str(csv_file)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_docx_file(temp_dir):
|
|
"""Create a mock DOCX file structure for testing."""
|
|
docx_file = temp_dir / "test.docx"
|
|
|
|
# Create a minimal ZIP structure that resembles a DOCX
|
|
import zipfile
|
|
with zipfile.ZipFile(docx_file, 'w') as zf:
|
|
# Minimal document.xml
|
|
document_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:body>
|
|
<w:p>
|
|
<w:r>
|
|
<w:t>Test document content for testing purposes.</w:t>
|
|
</w:r>
|
|
</w:p>
|
|
</w:body>
|
|
</w:document>'''
|
|
zf.writestr('word/document.xml', document_xml)
|
|
|
|
# Minimal content types
|
|
content_types = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
</Types>'''
|
|
zf.writestr('[Content_Types].xml', content_types)
|
|
|
|
# Minimal relationships
|
|
rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
</Relationships>'''
|
|
zf.writestr('_rels/.rels', rels)
|
|
|
|
return str(docx_file)
|
|
|
|
|
|
@pytest.fixture
|
|
def fast_mcp_app():
|
|
"""Create a clean FastMCP app instance for testing."""
|
|
return FastMCP("Test MCP Office Tools")
|
|
|
|
|
|
@pytest.fixture
|
|
def universal_mixin(fast_mcp_app):
|
|
"""Create a UniversalMixin instance for testing."""
|
|
return UniversalMixin(fast_mcp_app)
|
|
|
|
|
|
@pytest.fixture
|
|
def word_mixin(fast_mcp_app):
|
|
"""Create a WordMixin instance for testing."""
|
|
return WordMixin(fast_mcp_app)
|
|
|
|
|
|
@pytest.fixture
|
|
def composed_app():
|
|
"""Create a fully composed FastMCP app with all mixins."""
|
|
app = FastMCP("Composed Test App")
|
|
|
|
# Initialize all mixins
|
|
UniversalMixin(app)
|
|
WordMixin(app)
|
|
ExcelMixin(app)
|
|
PowerPointMixin(app)
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def test_session(composed_app):
|
|
"""Create a test session wrapper for FastMCP app testing."""
|
|
# Simple wrapper to test tools directly since FastMCP testing utilities
|
|
# may not be available in all versions
|
|
class TestSession:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
async def call_tool(self, tool_name: str, params: dict):
|
|
"""Call a tool directly for testing."""
|
|
if tool_name not in self.app._tools:
|
|
raise ValueError(f"Tool '{tool_name}' not found")
|
|
|
|
tool = self.app._tools[tool_name]
|
|
return await tool(**params)
|
|
|
|
return TestSession(composed_app)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_file_validation():
|
|
"""Standard mock for file validation."""
|
|
return {
|
|
"is_valid": True,
|
|
"errors": [],
|
|
"warnings": [],
|
|
"password_protected": False,
|
|
"file_size": 1024
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_format_detection():
|
|
"""Standard mock for format detection."""
|
|
return {
|
|
"category": "word",
|
|
"extension": ".docx",
|
|
"format_name": "Microsoft Word Document",
|
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
"is_legacy": False,
|
|
"structure": {
|
|
"estimated_complexity": "simple",
|
|
"has_images": False,
|
|
"has_tables": False
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_text_extraction_result():
|
|
"""Standard mock for text extraction results."""
|
|
return {
|
|
"text": "This is extracted text content from the document.",
|
|
"method_used": "python-docx",
|
|
"methods_tried": ["python-docx"],
|
|
"character_count": 45,
|
|
"word_count": 9,
|
|
"formatted_sections": [
|
|
{"type": "paragraph", "text": "This is extracted text content from the document."}
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_document_metadata():
|
|
"""Standard mock for document metadata."""
|
|
return {
|
|
"title": "Test Document",
|
|
"author": "Test Author",
|
|
"created": "2024-01-01T10:00:00Z",
|
|
"modified": "2024-01-15T14:30:00Z",
|
|
"subject": "Testing",
|
|
"keywords": ["test", "document"],
|
|
"word_count": 150,
|
|
"page_count": 2,
|
|
"file_size": 2048
|
|
}
|
|
|
|
|
|
class MockValidationContext:
|
|
"""Context manager for mocking validation utilities."""
|
|
|
|
def __init__(self,
|
|
resolve_path=None,
|
|
validation_result=None,
|
|
format_detection=None):
|
|
self.resolve_path = resolve_path
|
|
self.validation_result = validation_result or {"is_valid": True, "errors": []}
|
|
self.format_detection = format_detection or {
|
|
"category": "word",
|
|
"extension": ".docx",
|
|
"format_name": "Word Document"
|
|
}
|
|
self.patches = []
|
|
|
|
def __enter__(self):
|
|
import mcp_office_tools.utils.validation
|
|
import mcp_office_tools.utils.file_detection
|
|
|
|
from unittest.mock import patch
|
|
|
|
if self.resolve_path:
|
|
p1 = patch('mcp_office_tools.utils.validation.resolve_office_file_path',
|
|
return_value=self.resolve_path)
|
|
self.patches.append(p1)
|
|
p1.start()
|
|
|
|
p2 = patch('mcp_office_tools.utils.validation.validate_office_file',
|
|
return_value=self.validation_result)
|
|
self.patches.append(p2)
|
|
p2.start()
|
|
|
|
p3 = patch('mcp_office_tools.utils.file_detection.detect_format',
|
|
return_value=self.format_detection)
|
|
self.patches.append(p3)
|
|
p3.start()
|
|
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
for patch in self.patches:
|
|
patch.stop()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_validation_context():
|
|
"""Factory for creating MockValidationContext instances."""
|
|
return MockValidationContext
|
|
|
|
|
|
# FastMCP-specific test markers
|
|
pytest_plugins = ["pytest_asyncio"]
|
|
|
|
# Configure pytest markers
|
|
def pytest_configure(config):
|
|
"""Configure custom pytest markers."""
|
|
config.addinivalue_line(
|
|
"markers", "unit: mark test as a unit test"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "integration: mark test as an integration test"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "mixin: mark test as a mixin-specific test"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "tool_functionality: mark test as testing tool functionality"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "error_handling: mark test as testing error handling"
|
|
)
|
|
|
|
|
|
# Performance configuration for tests
|
|
@pytest.fixture(autouse=True)
|
|
def fast_test_execution():
|
|
"""Configure tests for fast execution."""
|
|
# Set shorter timeouts for async operations during testing
|
|
import asyncio
|
|
|
|
# Store original timeout
|
|
original_timeout = None
|
|
|
|
# Set test timeout (optional, based on your needs)
|
|
# You can customize this based on your test requirements
|
|
|
|
yield
|
|
|
|
# Restore original timeout if it was modified
|
|
if original_timeout is not None:
|
|
pass # Restore if needed
|
|
|
|
|
|
@pytest.fixture
|
|
def disable_real_file_operations():
|
|
"""Fixture to ensure no real file operations occur during testing."""
|
|
# This fixture can be used to patch file system operations
|
|
# to prevent accidental file creation/modification during tests
|
|
pass # Implementation depends on your specific needs |