""" Tests for LocalStorageBackend. TDD Phase 1: RED - Write tests first, then implement to pass. """ import shutil import tempfile from pathlib import Path import pytest @pytest.fixture def temp_storage_dir() -> Path: """Create a temporary directory for storage tests.""" temp_dir = Path(tempfile.mkdtemp()) yield temp_dir shutil.rmtree(temp_dir, ignore_errors=True) @pytest.fixture def sample_file(temp_storage_dir: Path) -> Path: """Create a sample file for testing.""" file_path = temp_storage_dir / "sample.txt" file_path.write_text("Hello, World!") return file_path @pytest.fixture def sample_image(temp_storage_dir: Path) -> Path: """Create a sample PNG file for testing.""" file_path = temp_storage_dir / "sample.png" # Minimal valid PNG (1x1 transparent pixel) png_data = bytes( [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, # PNG signature 0x00, 0x00, 0x00, 0x0D, # IHDR length 0x49, 0x48, 0x44, 0x52, # IHDR 0x00, 0x00, 0x00, 0x01, # width: 1 0x00, 0x00, 0x00, 0x01, # height: 1 0x08, 0x06, 0x00, 0x00, 0x00, # 8-bit RGBA 0x1F, 0x15, 0xC4, 0x89, # CRC 0x00, 0x00, 0x00, 0x0A, # IDAT length 0x49, 0x44, 0x41, 0x54, # IDAT 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, # compressed data 0x0D, 0x0A, 0x2D, 0xB4, # CRC 0x00, 0x00, 0x00, 0x00, # IEND length 0x49, 0x45, 0x4E, 0x44, # IEND 0xAE, 0x42, 0x60, 0x82, # CRC ] ) file_path.write_bytes(png_data) return file_path class TestLocalStorageBackendCreation: """Tests for LocalStorageBackend instantiation.""" def test_create_with_base_path(self, temp_storage_dir: Path) -> None: """Test creating backend with base path.""" from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) assert backend.base_path == temp_storage_dir def test_create_with_string_path(self, temp_storage_dir: Path) -> None: """Test creating backend with string path.""" from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=str(temp_storage_dir)) assert backend.base_path == temp_storage_dir def test_create_creates_directory_if_not_exists( self, temp_storage_dir: Path ) -> None: """Test that base directory is created if it doesn't exist.""" from shared.storage.local import LocalStorageBackend new_dir = temp_storage_dir / "new_storage" assert not new_dir.exists() backend = LocalStorageBackend(base_path=new_dir) assert new_dir.exists() assert backend.base_path == new_dir def test_is_storage_backend_subclass(self, temp_storage_dir: Path) -> None: """Test that LocalStorageBackend is a StorageBackend.""" from shared.storage.base import StorageBackend from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) assert isinstance(backend, StorageBackend) class TestLocalStorageBackendUpload: """Tests for LocalStorageBackend.upload method.""" def test_upload_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test uploading a file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) result = backend.upload(sample_file, "uploads/sample.txt") assert result == "uploads/sample.txt" assert (storage_dir / "uploads" / "sample.txt").exists() assert (storage_dir / "uploads" / "sample.txt").read_text() == "Hello, World!" def test_upload_creates_subdirectories( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that upload creates necessary subdirectories.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) result = backend.upload(sample_file, "deep/nested/path/sample.txt") assert (storage_dir / "deep" / "nested" / "path" / "sample.txt").exists() def test_upload_fails_if_file_exists_without_overwrite( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that upload fails if file exists and overwrite is False.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) # First upload succeeds backend.upload(sample_file, "sample.txt") # Second upload should fail with pytest.raises(StorageError, match="already exists"): backend.upload(sample_file, "sample.txt", overwrite=False) def test_upload_succeeds_with_overwrite( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that upload succeeds with overwrite=True.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) # First upload backend.upload(sample_file, "sample.txt") # Modify original file sample_file.write_text("Modified content") # Second upload with overwrite result = backend.upload(sample_file, "sample.txt", overwrite=True) assert result == "sample.txt" assert (storage_dir / "sample.txt").read_text() == "Modified content" def test_upload_nonexistent_file_fails(self, temp_storage_dir: Path) -> None: """Test that uploading nonexistent file fails.""" from shared.storage.base import FileNotFoundStorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(FileNotFoundStorageError): backend.upload(Path("/nonexistent/file.txt"), "sample.txt") def test_upload_binary_file( self, temp_storage_dir: Path, sample_image: Path ) -> None: """Test uploading a binary file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) result = backend.upload(sample_image, "images/sample.png") assert result == "images/sample.png" uploaded_content = (storage_dir / "images" / "sample.png").read_bytes() assert uploaded_content == sample_image.read_bytes() class TestLocalStorageBackendDownload: """Tests for LocalStorageBackend.download method.""" def test_download_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test downloading a file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" download_dir = temp_storage_dir / "downloads" download_dir.mkdir() backend = LocalStorageBackend(base_path=storage_dir) # First upload backend.upload(sample_file, "sample.txt") # Then download local_path = download_dir / "downloaded.txt" result = backend.download("sample.txt", local_path) assert result == local_path assert local_path.exists() assert local_path.read_text() == "Hello, World!" def test_download_creates_parent_directories( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that download creates parent directories.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "sample.txt") local_path = temp_storage_dir / "deep" / "nested" / "downloaded.txt" result = backend.download("sample.txt", local_path) assert local_path.exists() assert local_path.read_text() == "Hello, World!" def test_download_nonexistent_file_fails(self, temp_storage_dir: Path) -> None: """Test that downloading nonexistent file fails.""" from shared.storage.base import FileNotFoundStorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(FileNotFoundStorageError, match="nonexistent.txt"): backend.download("nonexistent.txt", Path("/tmp/file.txt")) def test_download_nested_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test downloading a file from nested path.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "a/b/c/sample.txt") local_path = temp_storage_dir / "downloaded.txt" result = backend.download("a/b/c/sample.txt", local_path) assert local_path.read_text() == "Hello, World!" class TestLocalStorageBackendExists: """Tests for LocalStorageBackend.exists method.""" def test_exists_returns_true_for_existing_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test exists returns True for existing file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "sample.txt") assert backend.exists("sample.txt") is True def test_exists_returns_false_for_nonexistent_file( self, temp_storage_dir: Path ) -> None: """Test exists returns False for nonexistent file.""" from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) assert backend.exists("nonexistent.txt") is False def test_exists_with_nested_path( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test exists with nested path.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "a/b/sample.txt") assert backend.exists("a/b/sample.txt") is True assert backend.exists("a/b/other.txt") is False class TestLocalStorageBackendListFiles: """Tests for LocalStorageBackend.list_files method.""" def test_list_files_empty_storage(self, temp_storage_dir: Path) -> None: """Test listing files in empty storage.""" from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) assert backend.list_files("") == [] def test_list_files_returns_all_files( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test listing all files.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) # Upload multiple files backend.upload(sample_file, "file1.txt") backend.upload(sample_file, "file2.txt") backend.upload(sample_file, "subdir/file3.txt") files = backend.list_files("") assert len(files) == 3 assert "file1.txt" in files assert "file2.txt" in files assert "subdir/file3.txt" in files def test_list_files_with_prefix( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test listing files with prefix filter.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "images/a.png") backend.upload(sample_file, "images/b.png") backend.upload(sample_file, "labels/a.txt") files = backend.list_files("images/") assert len(files) == 2 assert "images/a.png" in files assert "images/b.png" in files assert "labels/a.txt" not in files def test_list_files_returns_sorted( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that list_files returns sorted list.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "c.txt") backend.upload(sample_file, "a.txt") backend.upload(sample_file, "b.txt") files = backend.list_files("") assert files == ["a.txt", "b.txt", "c.txt"] class TestLocalStorageBackendDelete: """Tests for LocalStorageBackend.delete method.""" def test_delete_existing_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test deleting an existing file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "sample.txt") result = backend.delete("sample.txt") assert result is True assert not (storage_dir / "sample.txt").exists() def test_delete_nonexistent_file_returns_false( self, temp_storage_dir: Path ) -> None: """Test deleting nonexistent file returns False.""" from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) result = backend.delete("nonexistent.txt") assert result is False def test_delete_nested_file( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test deleting a nested file.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "a/b/sample.txt") result = backend.delete("a/b/sample.txt") assert result is True assert not (storage_dir / "a" / "b" / "sample.txt").exists() class TestLocalStorageBackendGetUrl: """Tests for LocalStorageBackend.get_url method.""" def test_get_url_returns_file_path( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test get_url returns file:// URL.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "sample.txt") url = backend.get_url("sample.txt") # Should return file:// URL or absolute path assert "sample.txt" in url # URL should be usable to locate the file expected_path = storage_dir / "sample.txt" assert str(expected_path) in url or expected_path.as_uri() == url def test_get_url_nonexistent_file(self, temp_storage_dir: Path) -> None: """Test get_url for nonexistent file.""" from shared.storage.base import FileNotFoundStorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(FileNotFoundStorageError): backend.get_url("nonexistent.txt") class TestLocalStorageBackendUploadBytes: """Tests for LocalStorageBackend.upload_bytes method.""" def test_upload_bytes(self, temp_storage_dir: Path) -> None: """Test uploading bytes directly.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) data = b"Binary content here" result = backend.upload_bytes(data, "binary.dat") assert result == "binary.dat" assert (storage_dir / "binary.dat").read_bytes() == data def test_upload_bytes_creates_subdirectories( self, temp_storage_dir: Path ) -> None: """Test that upload_bytes creates subdirectories.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) data = b"content" backend.upload_bytes(data, "a/b/c/file.dat") assert (storage_dir / "a" / "b" / "c" / "file.dat").exists() class TestLocalStorageBackendDownloadBytes: """Tests for LocalStorageBackend.download_bytes method.""" def test_download_bytes( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test downloading file as bytes.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) backend.upload(sample_file, "sample.txt") data = backend.download_bytes("sample.txt") assert data == b"Hello, World!" def test_download_bytes_nonexistent(self, temp_storage_dir: Path) -> None: """Test downloading nonexistent file as bytes.""" from shared.storage.base import FileNotFoundStorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(FileNotFoundStorageError): backend.download_bytes("nonexistent.txt") class TestLocalStorageBackendSecurity: """Security tests for LocalStorageBackend - path traversal prevention.""" def test_path_traversal_with_dotdot_blocked( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that path traversal using ../ is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.upload(sample_file, "../escape.txt") def test_path_traversal_with_nested_dotdot_blocked( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that nested path traversal is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.upload(sample_file, "subdir/../../escape.txt") def test_path_traversal_with_many_dotdot_blocked( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that deeply nested path traversal is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.upload(sample_file, "a/b/c/../../../../escape.txt") def test_absolute_path_unix_blocked( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that absolute Unix paths are blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Absolute paths not allowed"): backend.upload(sample_file, "/etc/passwd") def test_absolute_path_windows_blocked( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that absolute Windows paths are blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Absolute paths not allowed"): backend.upload(sample_file, "C:\\Windows\\System32\\config") def test_download_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in download is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.download("../escape.txt", Path("/tmp/file.txt")) def test_exists_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in exists is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.exists("../escape.txt") def test_delete_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in delete is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.delete("../escape.txt") def test_get_url_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in get_url is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.get_url("../escape.txt") def test_upload_bytes_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in upload_bytes is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.upload_bytes(b"content", "../escape.txt") def test_download_bytes_path_traversal_blocked( self, temp_storage_dir: Path ) -> None: """Test that path traversal in download_bytes is blocked.""" from shared.storage.base import StorageError from shared.storage.local import LocalStorageBackend backend = LocalStorageBackend(base_path=temp_storage_dir) with pytest.raises(StorageError, match="Path traversal not allowed"): backend.download_bytes("../escape.txt") def test_valid_nested_path_still_works( self, temp_storage_dir: Path, sample_file: Path ) -> None: """Test that valid nested paths still work after security fix.""" from shared.storage.local import LocalStorageBackend storage_dir = temp_storage_dir / "storage" backend = LocalStorageBackend(base_path=storage_dir) # Valid nested paths should still work result = backend.upload(sample_file, "a/b/c/d/file.txt") assert result == "a/b/c/d/file.txt" assert (storage_dir / "a" / "b" / "c" / "d" / "file.txt").exists()