Some checks are pending
Test Dashboard / test-and-dashboard (push) Waiting to run
Named for Milton Waddams, who was relocated to the basement with boxes of legacy documents. He handles the .doc and .xls files from 1997 that nobody else wants to touch. - Rename package from mcp-office-tools to mcwaddams - Update author to Ryan Malloy - Update all imports and references - Add Office Space themed README narrative - All 53 tests passing
296 lines
8.9 KiB
Python
296 lines
8.9 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 mcwaddams.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."""
|
|
mixin = UniversalMixin()
|
|
mixin.register_all(fast_mcp_app)
|
|
return mixin
|
|
|
|
|
|
@pytest.fixture
|
|
def word_mixin(fast_mcp_app):
|
|
"""Create a WordMixin instance for testing."""
|
|
mixin = WordMixin()
|
|
mixin.register_all(fast_mcp_app)
|
|
return mixin
|
|
|
|
|
|
@pytest.fixture
|
|
def composed_app():
|
|
"""Create a fully composed FastMCP app with all mixins."""
|
|
app = FastMCP("Composed Test App")
|
|
|
|
# Initialize and register all mixins
|
|
UniversalMixin().register_all(app)
|
|
WordMixin().register_all(app)
|
|
ExcelMixin().register_all(app)
|
|
PowerPointMixin().register_all(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._tool_manager._tools:
|
|
raise ValueError(f"Tool '{tool_name}' not found")
|
|
|
|
tool = self.app._tool_manager._tools[tool_name]
|
|
return await tool.fn(**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 mcwaddams.utils.validation
|
|
import mcwaddams.utils.file_detection
|
|
|
|
from unittest.mock import patch
|
|
|
|
if self.resolve_path:
|
|
p1 = patch('mcwaddams.utils.validation.resolve_office_file_path',
|
|
return_value=self.resolve_path)
|
|
self.patches.append(p1)
|
|
p1.start()
|
|
|
|
p2 = patch('mcwaddams.utils.validation.validate_office_file',
|
|
return_value=self.validation_result)
|
|
self.patches.append(p2)
|
|
p2.start()
|
|
|
|
p3 = patch('mcwaddams.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 and dashboard plugin
|
|
pytest_plugins = ["pytest_asyncio", "tests.pytest_dashboard_plugin"]
|
|
|
|
# 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 |