MCP Server Office
by famano
- tests
import os
import pytest
from mcp_server_office.office import validate_path, read_docx, write_docx, edit_docx_paragraph as edit_docx, edit_docx_insert, extract_table_text
from docx import Document
from docx.table import Table
from docx.oxml.shared import qn
from docx.oxml import OxmlElement
from docx.text.paragraph import Paragraph
@pytest.fixture
def sample_docx_with_track_changes():
"""Create a sample docx file with track changes for testing."""
path = "test_track_changes.docx"
doc = Document()
paragraph = doc.add_paragraph()
# Add text with track changes
run = OxmlElement('w:r')
text = OxmlElement('w:t')
text.text = "Original"
run.append(text)
paragraph._element.append(run)
# Add deletion
del_element = OxmlElement('w:del')
del_element.set(qn('w:author'), 'Test Author')
del_element.set(qn('w:date'), '2024-01-27T00:00:00Z')
del_run = OxmlElement('w:r')
del_text = OxmlElement('w:delText')
del_text.text = " deleted"
del_run.append(del_text)
del_element.append(del_run)
paragraph._element.append(del_element)
# Add insertion
ins_element = OxmlElement('w:ins')
ins_element.set(qn('w:author'), 'Test Author')
ins_element.set(qn('w:date'), '2024-01-27T00:00:00Z')
ins_run = OxmlElement('w:r')
ins_text = OxmlElement('w:t')
ins_text.text = " inserted"
ins_run.append(ins_text)
ins_element.append(ins_run)
paragraph._element.append(ins_element)
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.fixture
def sample_docx():
"""Create a sample docx file for testing."""
path = "test_sample.docx"
doc = Document()
doc.add_paragraph("Hello World")
# Add table
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "A1"
table.cell(0, 1).text = "B1"
table.cell(1, 0).text = "A2"
table.cell(1, 1).text = "B2"
doc.add_paragraph("Goodbye World")
doc.save(path)
yield path
# Cleanup
if os.path.exists(path):
os.remove(path)
@pytest.mark.asyncio
async def test_validate_path(sample_docx):
"""Test path validation."""
assert await validate_path(os.path.abspath(sample_docx)) == True
with pytest.raises(ValueError):
await validate_path(sample_docx)
with pytest.raises(ValueError):
await validate_path("nonexistent.docx")
@pytest.mark.asyncio
async def test_read_docx_with_track_changes(sample_docx_with_track_changes):
"""Test reading docx file with track changes."""
content = await read_docx(os.path.abspath(sample_docx_with_track_changes))
assert "Original" in content
assert not "deleted" in content
assert "inserted" in content
@pytest.mark.asyncio
async def test_read_docx(sample_docx):
"""Test reading docx file."""
content = await read_docx(os.path.abspath(sample_docx))
assert "Hello World" in content
assert "Goodbye World" in content
assert "[Table]" in content
assert "A1 | B1" in content
assert "A2 | B2" in content
with pytest.raises(ValueError):
await read_docx("nonexistent.docx")
@pytest.mark.asyncio
async def test_write_docx():
"""Test writing docx file."""
test_path = "test_write.docx"
test_content = "Test Paragraph\n\n[Table]\nC1 | D1\nC2 | D2\n\nFinal Paragraph"
try:
await write_docx(test_path, test_content)
assert os.path.exists(test_path)
# Verify content
doc = Document(test_path)
paragraphs = [p.text for p in doc.paragraphs if p.text]
assert "Test Paragraph" in paragraphs
assert "Final Paragraph" in paragraphs
# Verify table
table = doc.tables[0]
assert table.cell(0, 0).text == "C1"
assert table.cell(0, 1).text == "D1"
assert table.cell(1, 0).text == "C2"
assert table.cell(1, 1).text == "D2"
finally:
if os.path.exists(test_path):
os.remove(test_path)
@pytest.mark.asyncio
async def test_edit_docx_with_track_changes(sample_docx_with_track_changes):
"""Test editing docx file with track changes."""
abs_path = os.path.abspath(sample_docx_with_track_changes)
await edit_docx(abs_path, [{"paragraph_index": 0,"search": "Original", "replace": "Modified"}])
# Verify track changes are preserved
content = await read_docx(abs_path)
assert not "Original" in content
assert "Modified" in content
assert not "deleted" in content
assert "inserted" in content
@pytest.fixture
def complex_docx():
"""Create a sample docx file with complex content for testing cross-paragraph and table edits."""
path = "test_complex.docx"
doc = Document()
# Add paragraphs with text that spans multiple paragraphs
doc.add_paragraph("First part of")
doc.add_paragraph("a sentence that")
doc.add_paragraph("spans multiple paragraphs")
doc.add_paragraph("Some text before table")
# Add table with text that will be edited
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Table"
table.cell(0, 1).text = "Content"
table.cell(1, 0).text = "More"
table.cell(1, 1).text = "Text"
doc.add_paragraph("Some text after table")
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.fixture
def formatted_docx():
"""Create a sample docx file with formatted text for testing."""
path = "test_formatted.docx"
doc = Document()
# Add paragraph with formatted text
paragraph = doc.add_paragraph()
run = paragraph.add_run("Bold")
run.bold = True
run = paragraph.add_run(" and ")
run = paragraph.add_run("Italic")
run.italic = True
run = paragraph.add_run(" text")
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.mark.asyncio
async def test_edit_docx(sample_docx):
abs_sample_docx = os.path.abspath(sample_docx)
"""Test editing docx file with paragraph index."""
# Test single edit with paragraph index
result = await edit_docx(abs_sample_docx, [{"paragraph_index": 0, "search": "Hello", "replace": "Hi"}])
assert "-Hello" in result
assert "+Hi" in result
# Test multiple edits in different paragraphs
result = await edit_docx(abs_sample_docx, [
{"paragraph_index": 0, "search": "Hi", "replace": "Hellow"},
{"paragraph_index": 2, "search": "Goodbye", "replace": "Bye"}
])
assert "-Hi" in result
assert "+Hellow" in result
assert "-Goodbye" in result
assert "+Bye" in result
# Test non-existent text in paragraph
with pytest.raises(ValueError) as exc_info:
await edit_docx(abs_sample_docx, [{"paragraph_index": 0, "search": "NonexistentText", "replace": "NewText"}])
assert "'NonexistentText' in paragraph 0" in str(exc_info.value)
# Test invalid paragraph index
with pytest.raises(ValueError) as exc_info:
await edit_docx(abs_sample_docx, [{"paragraph_index": 999, "search": "Hello", "replace": "Hi"}])
assert "Paragraph index out of range: 999" in str(exc_info.value)
# Test invalid file
with pytest.raises(ValueError):
await edit_docx("nonexistent.docx", [{"paragraph_index": 0, "search": "Hello", "replace": "Hi"}])
@pytest.mark.asyncio
async def test_edit_docx_format_preservation(formatted_docx):
"""Test that formatting is preserved when editing text."""
abs_formatted_docx = os.path.abspath(formatted_docx)
# Edit text while preserving bold formatting
result = await edit_docx(abs_formatted_docx, [
{"paragraph_index": 0, "search": "Bold and Italic text", "replace": "Modified text"}
])
# Verify content was changed
assert "-Bold and Italic text" in result
assert "+Modified text" in result
# Verify formatting was preserved by checking the document directly
doc = Document(abs_formatted_docx)
paragraph = doc.paragraphs[0]
assert len(paragraph.runs) == 1 # Should be consolidated into a single run
run = paragraph.runs[0]
assert run.bold # Should inherit bold formatting from first run
@pytest.mark.asyncio
async def test_edit_docx_table_content(complex_docx):
"""Test editing text within table cells."""
abs_complex_docx = os.path.abspath(complex_docx)
# Test editing table content
result = await edit_docx(abs_complex_docx, [
{"paragraph_index": 4,"search": "Table | Content", "replace": "Modified | Cell"},
{"paragraph_index": 4,"search": "More | Text", "replace": "Modification | Two"},
])
assert "-Table | Content" in result
assert "+Modified | Cell" in result
assert "-More | Text" in result
assert "+Modification | Two" in result
@pytest.fixture
def table_at_start_docx():
"""Create a docx file that starts with a table."""
path = "test_table_at_start.docx"
doc = Document()
# Add table at the start
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Start1"
table.cell(0, 1).text = "Start2"
table.cell(1, 0).text = "Start3"
table.cell(1, 1).text = "Start4"
# Add some text after table
doc.add_paragraph("Text after table")
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.fixture
def table_after_empty_paragraph_docx():
"""Create a docx file with empty paragraph before table."""
path = "test_table_after_empty.docx"
doc = Document()
# Add empty paragraph
doc.add_paragraph("")
# Add table after empty paragraph
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Empty1"
table.cell(0, 1).text = "Empty2"
table.cell(1, 0).text = "Empty3"
table.cell(1, 1).text = "Empty4"
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.mark.asyncio
async def test_edit_docx_table_at_start(table_at_start_docx):
"""Test editing table that appears at the start of document."""
abs_path = os.path.abspath(table_at_start_docx)
# Test editing the table content
result = await edit_docx(abs_path, [
{"paragraph_index": 0, "search": "Start1 | Start2", "replace": "Modified1 | Modified2"}
])
# Verify changes
assert "-Start1 | Start2" in result
assert "+Modified1 | Modified2" in result
# Verify the document structure is maintained
content = await read_docx(abs_path)
assert "Text after table" in content # Verify text after table is preserved
@pytest.mark.asyncio
async def test_edit_docx_table_after_empty_paragraph(table_after_empty_paragraph_docx):
"""Test editing table that appears after an empty paragraph."""
abs_path = os.path.abspath(table_after_empty_paragraph_docx)
# Test editing the table content
result = await edit_docx(abs_path, [
{"paragraph_index": 1, "search": "Empty1 | Empty2", "replace": "Modified1 | Modified2"}
])
# Verify changes
assert "-Empty1 | Empty2" in result
assert "+Modified1 | Modified2" in result
# Verify the document structure is maintained
content = await read_docx(abs_path)
assert "Modified1" in content
assert "Modified2" in content
@pytest.fixture
def deleted_text_before_table_docx():
"""Create a docx file with deleted text in a paragraph before table."""
path = "test_deleted_text_before_table.docx"
doc = Document()
# 削除された文字を含む段落を追加
paragraph = doc.add_paragraph()
# 削除マークを付けたテキストを追加
del_element = OxmlElement('w:del')
del_element.set(qn('w:author'), 'Test Author')
del_element.set(qn('w:date'), '2024-01-27T00:00:00Z')
del_run = OxmlElement('w:r')
del_text = OxmlElement('w:delText')
del_text.text = "This text is deleted"
del_run.append(del_text)
del_element.append(del_run)
paragraph._element.append(del_element)
# テーブルを追加
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Test1"
table.cell(0, 1).text = "Test2"
table.cell(1, 0).text = "Test3"
table.cell(1, 1).text = "Test4"
doc.save(path)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.mark.asyncio
async def test_edit_docx_table_after_deleted_text(deleted_text_before_table_docx):
"""Test editing table that appears after a paragraph with deleted text."""
abs_path = os.path.abspath(deleted_text_before_table_docx)
# テーブルの内容を編集
result = await edit_docx(abs_path, [
{"paragraph_index": 1, "search": "Test1 | Test2", "replace": "Modified1 | Modified2"}
])
# 変更を確認
assert "-Test1 | Test2" in result
assert "+Modified1 | Modified2" in result
# ドキュメント構造が維持されていることを確認
content = await read_docx(abs_path)
assert "Modified1" in content
assert "Modified2" in content
# 削除されたテキストが表示されないことを確認
assert "This text is deleted" not in content
def test_extract_table_text():
"""Test table text extraction."""
doc = Document()
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "X1"
table.cell(0, 1).text = "Y1"
table.cell(1, 0).text = "X2"
table.cell(1, 1).text = "Y2"
result = extract_table_text(table)
assert "X1 | Y1" in result
assert "X2 | Y2" in result
@pytest.mark.asyncio
async def test_edit_docx_insert_basic(sample_docx):
"""Test basic paragraph insertion."""
abs_path = os.path.abspath(sample_docx)
result = await edit_docx_insert(abs_path, [
{"text": "Inserted Text", "paragraph_index": 1}
])
# 変更を確認
content = await read_docx(abs_path)
assert "Inserted Text" in content
assert "+Inserted Text" in result
@pytest.mark.asyncio
async def test_edit_docx_insert_multiple(sample_docx):
"""Test inserting multiple paragraphs at different positions."""
abs_path = os.path.abspath(sample_docx)
result = await edit_docx_insert(abs_path, [
{"text": "First Insert", "paragraph_index": 0},
{"text": "Second Insert", "paragraph_index": 2}
])
content = await read_docx(abs_path)
assert "First Insert" in content
assert "Second Insert" in content
assert content.index("First Insert") < content.index("Second Insert")
@pytest.mark.asyncio
async def test_edit_docx_insert_same_position(sample_docx):
"""Test inserting multiple paragraphs at the same position."""
abs_path = os.path.abspath(sample_docx)
result = await edit_docx_insert(abs_path, [
{"text": "First Same Position"},
{"text": "Second Same Position"},
{"text": "Third Same Position"}
])
content = await read_docx(abs_path)
# 指定順序で挿入されていることを確認
assert content.index("First Same Position") < content.index("Second Same Position")
assert content.index("Second Same Position") < content.index("Third Same Position")
@pytest.mark.asyncio
async def test_edit_docx_insert_at_end(sample_docx):
"""Test inserting paragraph at the end of document."""
abs_path = os.path.abspath(sample_docx)
result = await edit_docx_insert(abs_path, [
{"text": "End of Document"}
])
content = await read_docx(abs_path)
assert "End of Document" in content
assert content.rindex("End of Document") > content.rindex("Goodbye World")
@pytest.mark.asyncio
async def test_edit_docx_insert_before_table(complex_docx):
"""Test inserting paragraph before table."""
abs_path = os.path.abspath(complex_docx)
result = await edit_docx_insert(abs_path, [
{"text": "Before Table Text", "paragraph_index": 4}
])
content = await read_docx(abs_path)
assert "Before Table Text" in content
# テーブルの前に挿入されていることを確認
table_index = content.index("[Table]")
insert_index = content.index("Before Table Text")
assert insert_index < table_index
@pytest.mark.asyncio
async def test_edit_docx_insert_errors(sample_docx):
"""Test error cases for paragraph insertion."""
# 存在しないファイル
with pytest.raises(ValueError) as exc_info:
await edit_docx_insert(os.path.abspath("nonexistent.docx"), [{"text": "Test"}])
assert "File not found" in str(exc_info.value)
# 範囲外のインデックス
abs_path = os.path.abspath(sample_docx)
with pytest.raises(ValueError) as exc_info:
await edit_docx_insert(abs_path, [{"text": "Test", "paragraph_index": 999}])
assert "Paragraph index out of range" in str(exc_info.value)