MCP Filesystem Server
by safurrier
Verified
- mcp-filesystem
- tests
"""Unit tests for FileOperations class.
These tests verify the core file operations functionality using behavioral
testing techniques focused on outcomes rather than implementation details.
"""
import os
import json
import tempfile
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
import pytest
from unittest.mock import AsyncMock, MagicMock, patch, call
import anyio
from mcp_filesystem.operations import FileOperations, FileInfo
from mcp_filesystem.security import PathValidator
@pytest.fixture
def test_fs():
"""Create a temporary filesystem for testing file operations.
This fixture provides a real filesystem with a standard set of
test files and directories to avoid excessive mocking.
"""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
# Create test files
test_file = base_dir / "test.txt"
test_file.write_text("This is a test file\nWith multiple lines\nFor testing")
# Create a subdirectory with content
subdir = base_dir / "subdir"
subdir.mkdir()
(subdir / "subfile.txt").write_text("File in subdirectory")
# Create a file for editing
edit_file = base_dir / "edit.txt"
edit_file.write_text("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
# Create a large file with many lines for testing read_file_lines
lines_file = base_dir / "lines.txt"
lines_file.write_text("\n".join([f"Line {i}" for i in range(1, 101)]))
# Create a directory for move operations
move_dir = base_dir / "move_test"
move_dir.mkdir()
yield base_dir
@pytest.fixture
def file_operations(test_fs):
"""Create a FileOperations instance with the test filesystem."""
validator = PathValidator([str(test_fs)])
return FileOperations(validator)
@pytest.mark.asyncio
class TestFileOperations:
"""Unit tests for FileOperations class using behavioral testing."""
async def test_read_file_returns_content(self, file_operations, test_fs):
"""Test that read_file returns the correct file content."""
# Arrange
test_path = str(test_fs / "test.txt")
expected_content = "This is a test file\nWith multiple lines\nFor testing"
# Act
content = await file_operations.read_file(test_path)
# Assert - focus on behavior, not implementation
assert content == expected_content
async def test_read_file_raises_error_for_nonexistent_file(self, file_operations, test_fs):
"""Test that read_file raises appropriate error for nonexistent files."""
# Arrange
test_path = str(test_fs / "nonexistent.txt")
# Act/Assert
with pytest.raises(FileNotFoundError):
await file_operations.read_file(test_path)
async def test_read_file_with_different_encoding(self, file_operations, test_fs):
"""Test read_file with different encoding."""
# Arrange
test_path = str(test_fs / "encoded.txt")
test_content = "Café Français" # Content with non-ASCII characters
# Create a file with specific encoding
Path(test_path).write_bytes(test_content.encode('latin-1'))
# Act
content = await file_operations.read_file(test_path, encoding='latin-1')
# Assert
assert content == test_content
async def test_write_file_creates_new_file(self, file_operations, test_fs):
"""Test that write_file creates a new file with correct content."""
# Arrange
test_path = str(test_fs / "new_file.txt")
test_content = "New file content"
# Act
await file_operations.write_file(test_path, test_content)
# Assert - check the file was actually created with correct content
assert Path(test_path).exists()
assert Path(test_path).read_text() == test_content
async def test_write_file_with_create_dirs(self, file_operations, test_fs):
"""Test write_file with create_dirs option."""
# Arrange
new_dir = test_fs / "new_directory"
test_path = str(new_dir / "nested_file.txt")
test_content = "Content in a nested directory"
# Act
await file_operations.write_file(test_path, test_content, create_dirs=True)
# Assert
assert new_dir.exists()
assert new_dir.is_dir()
assert Path(test_path).exists()
assert Path(test_path).read_text() == test_content
async def test_list_directory_returns_file_entries(self, file_operations, test_fs):
"""Test that list_directory returns correct file and directory entries."""
# Arrange - we already have a test filesystem set up
# Act
entries = await file_operations.list_directory(str(test_fs))
# Assert - check behavior, not implementation
assert isinstance(entries, list)
# Check the basic files we created are listed
file_names = [entry["name"] for entry in entries]
assert "test.txt" in file_names
assert "subdir" in file_names
assert "edit.txt" in file_names
assert "lines.txt" in file_names
# Verify the entries have the expected structure
for entry in entries:
if entry["name"] == "subdir":
assert entry["is_directory"] is True
elif entry["name"] == "test.txt":
assert entry["is_file"] is True
assert entry["size"] > 0
async def test_list_directory_with_pattern_filter(self, file_operations, test_fs):
"""Test list_directory with a pattern filter."""
# Arrange
# Create additional files of different types for pattern matching
(test_fs / "doc1.md").write_text("Markdown file")
(test_fs / "doc2.md").write_text("Another markdown file")
# Act - filter by *.md pattern
entries = await file_operations.list_directory(str(test_fs), pattern="*.md")
# Assert
assert len(entries) == 2
file_names = [entry["name"] for entry in entries]
assert "doc1.md" in file_names
assert "doc2.md" in file_names
assert "test.txt" not in file_names
async def test_move_file(self, file_operations, test_fs):
"""Test moving a file from one location to another."""
# Arrange
source_path = str(test_fs / "move_source.txt")
dest_path = str(test_fs / "move_test" / "move_destination.txt")
test_content = "File to be moved"
# Create a test file to move
Path(source_path).write_text(test_content)
# Act
await file_operations.move_file(source_path, dest_path)
# Assert
assert not Path(source_path).exists()
assert Path(dest_path).exists()
assert Path(dest_path).read_text() == test_content
async def test_get_file_info_returns_metadata(self, file_operations, test_fs):
"""Test that get_file_info returns correct file metadata."""
# Arrange
test_path = str(test_fs / "test.txt")
# Act
file_info = await file_operations.get_file_info(test_path)
# Assert
assert file_info.name == "test.txt"
assert str(file_info.path).endswith("test.txt")
assert file_info.is_file is True
assert file_info.is_dir is False
assert file_info.size > 0
assert isinstance(file_info.created, datetime)
assert isinstance(file_info.modified, datetime)
assert isinstance(file_info.permissions, str)
# Check the dict representation contains the same info
info_dict = file_info.to_dict()
assert info_dict["name"] == "test.txt"
assert "size" in info_dict
assert "created" in info_dict
assert "modified" in info_dict
async def test_read_file_lines_with_offset_and_limit(self, file_operations, test_fs):
"""Test reading specific lines from a file with offset and limit."""
# Arrange
test_path = str(test_fs / "lines.txt")
offset = 5 # Start at line 6 (0-indexed)
limit = 3 # Read 3 lines
# Act
content, metadata = await file_operations.read_file_lines(test_path, offset, limit)
# Assert
# Check content behavior - should contain lines 6-8, allowing for potential trailing newline
assert "Line 6" in content
assert "Line 7" in content
assert "Line 8" in content
assert "Line 5" not in content
assert "Line 9" not in content
# Check metadata behavior rather than exact implementation
assert metadata["offset"] == offset
assert metadata["limit"] == limit
assert metadata["lines_read"] == limit
async def test_edit_file_at_line_replaces_line(self, file_operations, test_fs):
"""Test editing a specific line in a file."""
# Arrange
test_path = str(test_fs / "edit.txt")
# Reset the file to a known state
original_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
Path(test_path).write_text(original_content)
line_edits = [
{"line_number": 2, "action": "replace", "content": "Modified Line 2"}
]
# Act
result = await file_operations.edit_file_at_line(test_path, line_edits)
# Assert
# Check the file was modified correctly by verifying behavior
new_content = Path(test_path).read_text()
assert "Modified Line 2" in new_content
assert "Line 1" in new_content # Unchanged line
assert "Line 3" in new_content # Unchanged line
# Check line 2 was replaced by verifying line ordering
lines = new_content.splitlines()
assert lines[0] == "Line 1"
assert lines[1] == "Modified Line 2"
assert lines[2] == "Line 3"
# Check the result contains correct metadata
assert result["edits_applied"] == 1
# Allow for macOS /var vs /private/var path normalization
assert Path(result["path"]).name == Path(test_path).name
changes = result.get("changes", [])
assert len(changes) == 1
assert changes[0]["line"] == 2
assert changes[0]["action"] == "replace"
async def test_edit_file_at_line_with_verification(self, file_operations, test_fs):
"""Test line editing with content verification."""
# Arrange
test_path = str(test_fs / "edit.txt")
# Edit with correct verification
correct_edit = [
{
"line_number": 3,
"action": "replace",
"content": "Modified Line 3",
"expected_content": "Line 3"
}
]
# Edit with incorrect verification
incorrect_edit = [
{
"line_number": 3,
"action": "replace",
"content": "Modified Line 3",
"expected_content": "Wrong content"
}
]
# Act - First try with correct verification
result_success = await file_operations.edit_file_at_line(
test_path, correct_edit, abort_on_verification_failure=True
)
# Get the content after successful edit
content_after_success = Path(test_path).read_text()
# Act - Now try with incorrect verification
result_failure = await file_operations.edit_file_at_line(
test_path, incorrect_edit, abort_on_verification_failure=True
)
# Get the content after failed edit
content_after_failure = Path(test_path).read_text()
# Assert
# Successful edit should change the file
assert "Modified Line 3" in content_after_success
assert result_success["edits_applied"] == 1
# Failed verification should not change the file
assert content_after_success == content_after_failure
assert not result_failure.get("success", True)
assert "verification_failures" in result_failure
async def test_edit_file_at_line_with_relative_numbering(self, file_operations, test_fs):
"""Test editing with relative line numbering."""
# Arrange
test_path = str(test_fs / "edit.txt")
# Reset the content to ensure consistent state
Path(test_path).write_text("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
offset = 2 # Start at Line 3 (0-indexed position 2)
# Edit with relative line numbers
relative_edits = [
{"line_number": 1, "action": "replace", "content": "Modified Line 4"}
]
# Act
result = await file_operations.edit_file_at_line(
test_path, relative_edits,
offset=offset, relative_line_numbers=True
)
# Assert
# Check content by verifying actual line ordering
new_content = Path(test_path).read_text()
lines = new_content.splitlines()
assert lines[0] == "Line 1"
assert lines[1] == "Line 2"
assert lines[2] == "Line 3"
assert lines[3] == "Modified Line 4" # Fourth line should be modified
assert lines[4] == "Line 5"
# Check metadata
assert result["edits_applied"] == 1
changes = result.get("changes", [])
# Line number in result should reference an absolute line number
assert changes[0]["line"] == 4
@pytest.mark.asyncio
class TestFileInfo:
"""Tests for the FileInfo class."""
async def test_fileinfo_gets_correct_metadata(self, test_fs):
"""Test that FileInfo correctly retrieves file metadata."""
# Arrange - create a test file with known content
test_file = test_fs / "fileinfo_test.txt"
test_content = "Test content for FileInfo"
test_file.write_text(test_content)
# Act
file_info = FileInfo(test_file)
# Assert
assert file_info.name == "fileinfo_test.txt"
assert file_info.is_file is True
assert file_info.is_dir is False
assert file_info.size == len(test_content)
assert isinstance(file_info.created, datetime)
assert isinstance(file_info.modified, datetime)
assert isinstance(file_info.permissions, str)
async def test_fileinfo_for_directory(self, test_fs):
"""Test that FileInfo correctly identifies a directory."""
# Arrange - use an existing directory
test_dir = test_fs / "subdir"
# Act
dir_info = FileInfo(test_dir)
# Assert
assert dir_info.name == "subdir"
assert dir_info.is_dir is True
assert dir_info.is_file is False
async def test_fileinfo_to_dict(self, test_fs):
"""Test FileInfo.to_dict returns a complete dictionary representation."""
# Arrange
test_file = test_fs / "fileinfo_dict.txt"
test_file.write_text("Test content")
# Act
file_info = FileInfo(test_file)
info_dict = file_info.to_dict()
# Assert
assert isinstance(info_dict, dict)
assert info_dict["name"] == "fileinfo_dict.txt"
assert info_dict["path"] == str(test_file)
assert info_dict["is_file"] is True
assert info_dict["is_directory"] is False
assert "size" in info_dict
assert "created" in info_dict
assert "modified" in info_dict
assert "permissions" in info_dict
async def test_fileinfo_string_representation(self, test_fs):
"""Test the string representation of FileInfo."""
# Arrange
test_file = test_fs / "fileinfo_str.txt"
test_file.write_text("Test content")
# Act
file_info = FileInfo(test_file)
info_str = str(file_info)
# Assert
assert "File" in info_str
assert "fileinfo_str.txt" in info_str
assert "Size" in info_str
assert "Created" in info_str
assert "Modified" in info_str
assert "Permissions" in info_str
if __name__ == "__main__":
pytest.main(["-xvs", __file__])