Ryan Malloy 0748eec48d Fix FastMCP stdio server import
- 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
2025-09-26 15:49:00 -06:00

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