Skip to main content
Glama
test_path_resolver.py10 kB
"""Test secure path resolver for filesystem operations.""" from pathlib import Path import pytest from mcp_server_whisper.infrastructure.path_resolver import SecurePathResolver class TestSecurePathResolver: """Test suite for SecurePathResolver security and functionality.""" @pytest.fixture def base_path(self, tmp_path: Path) -> Path: """Create a temporary base directory for testing.""" audio_dir = tmp_path / "audio" audio_dir.mkdir() return audio_dir @pytest.fixture def resolver(self, base_path: Path) -> SecurePathResolver: """Create a SecurePathResolver instance.""" return SecurePathResolver(base_path) @pytest.fixture def sample_file(self, base_path: Path) -> Path: """Create a sample file for testing.""" file_path = base_path / "test.mp3" file_path.write_text("test content") return file_path def test_resolve_input_valid_filename(self, resolver: SecurePathResolver, sample_file: Path) -> None: """Test resolving a valid filename.""" result = resolver.resolve_input("test.mp3") assert result == sample_file assert result.exists() def test_resolve_input_file_not_found(self, resolver: SecurePathResolver) -> None: """Test that FileNotFoundError is raised for missing files.""" with pytest.raises(FileNotFoundError, match="File not found: nonexistent.mp3"): resolver.resolve_input("nonexistent.mp3") def test_resolve_input_prevents_parent_directory_traversal(self, resolver: SecurePathResolver) -> None: """Test that '../' path traversal is safely handled by stripping directory components.""" # The resolver strips directory components, so ../secret.txt becomes secret.txt # This is secure behavior - the .. is ignored with pytest.raises(FileNotFoundError, match="File not found: secret.txt"): resolver.resolve_input("../secret.txt") def test_resolve_input_prevents_absolute_path_traversal(self, resolver: SecurePathResolver, tmp_path: Path) -> None: """Test that absolute paths are safely handled by stripping directory components.""" # Create a file outside the base directory outside_file = tmp_path / "outside.txt" outside_file.write_text("secret") # Try to access it with absolute path - should strip to just filename # This is secure: /tmp/outside.txt becomes outside.txt (not found in base dir) with pytest.raises(FileNotFoundError, match="File not found: outside.txt"): resolver.resolve_input(str(outside_file)) def test_resolve_input_prevents_multi_level_traversal(self, resolver: SecurePathResolver) -> None: """Test that multi-level '../../../' traversal is safely handled.""" # The resolver strips all directory components, so ../../../etc/passwd becomes passwd # This is secure behavior - all the ../ are ignored with pytest.raises(FileNotFoundError, match="File not found: passwd"): resolver.resolve_input("../../../etc/passwd") def test_resolve_input_strips_directory_components(self, resolver: SecurePathResolver, sample_file: Path) -> None: """Test that directory components are stripped from input.""" # Even if user provides a path with directories, only the filename is used result = resolver.resolve_input("subdir/test.mp3") # Should resolve to base_path/test.mp3 (not base_path/subdir/test.mp3) assert result == sample_file def test_resolve_input_with_subdirectory_file(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test accessing a file that legitimately exists in base directory.""" # Create a file with a name that looks like a path tricky_file = base_path / "looks_like_path.mp3" tricky_file.write_text("content") result = resolver.resolve_input("looks_like_path.mp3") assert result == tricky_file assert result.exists() def test_resolve_output_with_custom_filename(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test resolving output path with custom filename.""" result = resolver.resolve_output("output.wav", default="default.mp3") assert result == base_path / "output.wav" # File doesn't need to exist for output def test_resolve_output_with_none_uses_default(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test that None filename uses default.""" result = resolver.resolve_output(None, default="default.mp3") assert result == base_path / "default.mp3" def test_resolve_output_prevents_path_traversal(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test that output path traversal is safely handled.""" # The resolver strips directory components, so ../malicious.mp3 becomes malicious.mp3 # This is secure - it stays in the base directory result = resolver.resolve_output("../malicious.mp3", default="default.mp3") assert result == base_path / "malicious.mp3" # Verify it's still in the base directory assert str(result).startswith(str(base_path)) def test_resolve_output_strips_directory_components(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test that directory components are stripped from output filename.""" result = resolver.resolve_output("subdir/output.mp3", default="default.mp3") # Should be base_path/output.mp3, not base_path/subdir/output.mp3 assert result == base_path / "output.mp3" def test_resolve_output_with_absolute_path_attempt( self, resolver: SecurePathResolver, tmp_path: Path, base_path: Path ) -> None: """Test that absolute path attempts for output are safely handled.""" outside_path = str(tmp_path / "outside" / "output.mp3") # The resolver strips directory components, keeping only the filename # This is secure - /tmp/outside/output.mp3 becomes output.mp3 in base dir result = resolver.resolve_output(outside_path, default="default.mp3") assert result == base_path / "output.mp3" # Verify it's in the base directory, not the attempted outside path assert str(result).startswith(str(base_path)) assert "outside" not in str(result) def test_get_relative_name_returns_filename(self, resolver: SecurePathResolver) -> None: """Test that get_relative_name extracts just the filename.""" path = Path("/some/long/path/to/file.mp3") result = resolver.get_relative_name(path) assert result == "file.mp3" def test_get_relative_name_with_no_directory(self, resolver: SecurePathResolver) -> None: """Test get_relative_name with just a filename.""" path = Path("file.mp3") result = resolver.get_relative_name(path) assert result == "file.mp3" def test_base_path_is_resolved(self, tmp_path: Path) -> None: """Test that base path is resolved to absolute path.""" # Create a path with . and .. components complex_path = tmp_path / "subdir" / ".." / "audio" complex_path.mkdir(parents=True, exist_ok=True) resolver = SecurePathResolver(complex_path) # Base path should be resolved and normalized assert resolver.base_path.is_absolute() assert ".." not in str(resolver.base_path) def test_resolve_input_with_whitespace(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test resolving filenames with spaces.""" file_with_space = base_path / "my audio file.mp3" file_with_space.write_text("content") result = resolver.resolve_input("my audio file.mp3") assert result == file_with_space assert result.exists() def test_resolve_output_with_whitespace(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test resolving output paths with spaces.""" result = resolver.resolve_output("output file.mp3", default="default.mp3") assert result == base_path / "output file.mp3" def test_resolve_input_with_special_characters(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test resolving filenames with special characters.""" special_file = base_path / "test_file-123.mp3" special_file.write_text("content") result = resolver.resolve_input("test_file-123.mp3") assert result == special_file def test_multiple_resolvers_different_base_paths(self, tmp_path: Path) -> None: """Test that multiple resolvers maintain separate base paths.""" dir1 = tmp_path / "audio1" dir2 = tmp_path / "audio2" dir1.mkdir() dir2.mkdir() resolver1 = SecurePathResolver(dir1) resolver2 = SecurePathResolver(dir2) assert resolver1.base_path != resolver2.base_path assert resolver1.base_path == dir1.resolve() assert resolver2.base_path == dir2.resolve() def test_resolve_input_empty_filename(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test that empty filename is handled.""" # Empty string's .name is '.', which likely doesn't exist # Or it might refer to the directory itself try: result = resolver.resolve_input("") # If it succeeds, it should be resolving to base directory or similar assert str(result).startswith(str(base_path)) except FileNotFoundError: # This is also acceptable behavior pass def test_resolve_output_empty_filename_uses_default(self, resolver: SecurePathResolver, base_path: Path) -> None: """Test that empty output filename uses default.""" result = resolver.resolve_output("", default="default.mp3") # Empty string is falsy, so should use default assert result == base_path / "default.mp3"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/arcaputo3/mcp-server-whisper'

If you have feedback or need assistance with the MCP directory API, please join our Discord server