""" Tests for storage base module. TDD Phase 1: RED - Write tests first, then implement to pass. """ from abc import ABC from pathlib import Path from typing import BinaryIO from unittest.mock import MagicMock, patch import pytest class TestStorageBackendInterface: """Tests for StorageBackend abstract base class.""" def test_cannot_instantiate_directly(self) -> None: """Test that StorageBackend cannot be instantiated.""" from shared.storage.base import StorageBackend with pytest.raises(TypeError): StorageBackend() # type: ignore def test_is_abstract_base_class(self) -> None: """Test that StorageBackend is an ABC.""" from shared.storage.base import StorageBackend assert issubclass(StorageBackend, ABC) def test_subclass_must_implement_upload(self) -> None: """Test that subclass must implement upload method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def download(self, remote_path: str, local_path: Path) -> Path: return local_path def exists(self, remote_path: str) -> bool: return False def list_files(self, prefix: str) -> list[str]: return [] def delete(self, remote_path: str) -> bool: return True def get_url(self, remote_path: str) -> str: return "" with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_subclass_must_implement_download(self) -> None: """Test that subclass must implement download method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def exists(self, remote_path: str) -> bool: return False def list_files(self, prefix: str) -> list[str]: return [] def delete(self, remote_path: str) -> bool: return True def get_url(self, remote_path: str) -> str: return "" with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_subclass_must_implement_exists(self) -> None: """Test that subclass must implement exists method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def download(self, remote_path: str, local_path: Path) -> Path: return local_path def list_files(self, prefix: str) -> list[str]: return [] def delete(self, remote_path: str) -> bool: return True def get_url(self, remote_path: str) -> str: return "" with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_subclass_must_implement_list_files(self) -> None: """Test that subclass must implement list_files method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def download(self, remote_path: str, local_path: Path) -> Path: return local_path def exists(self, remote_path: str) -> bool: return False def delete(self, remote_path: str) -> bool: return True def get_url(self, remote_path: str) -> str: return "" with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_subclass_must_implement_delete(self) -> None: """Test that subclass must implement delete method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def download(self, remote_path: str, local_path: Path) -> Path: return local_path def exists(self, remote_path: str) -> bool: return False def list_files(self, prefix: str) -> list[str]: return [] def get_url(self, remote_path: str) -> str: return "" with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_subclass_must_implement_get_url(self) -> None: """Test that subclass must implement get_url method.""" from shared.storage.base import StorageBackend class IncompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def download(self, remote_path: str, local_path: Path) -> Path: return local_path def exists(self, remote_path: str) -> bool: return False def list_files(self, prefix: str) -> list[str]: return [] def delete(self, remote_path: str) -> bool: return True with pytest.raises(TypeError): IncompleteBackend() # type: ignore def test_valid_subclass_can_be_instantiated(self) -> None: """Test that a complete subclass can be instantiated.""" from shared.storage.base import StorageBackend class CompleteBackend(StorageBackend): def upload( self, local_path: Path, remote_path: str, overwrite: bool = False ) -> str: return remote_path def download(self, remote_path: str, local_path: Path) -> Path: return local_path def exists(self, remote_path: str) -> bool: return False def list_files(self, prefix: str) -> list[str]: return [] def delete(self, remote_path: str) -> bool: return True def get_url(self, remote_path: str) -> str: return "" def get_presigned_url( self, remote_path: str, expires_in_seconds: int = 3600 ) -> str: return "" backend = CompleteBackend() assert isinstance(backend, StorageBackend) class TestStorageError: """Tests for StorageError exception.""" def test_storage_error_is_exception(self) -> None: """Test that StorageError is an Exception.""" from shared.storage.base import StorageError assert issubclass(StorageError, Exception) def test_storage_error_with_message(self) -> None: """Test StorageError with message.""" from shared.storage.base import StorageError error = StorageError("Upload failed") assert str(error) == "Upload failed" def test_storage_error_can_be_raised(self) -> None: """Test that StorageError can be raised and caught.""" from shared.storage.base import StorageError with pytest.raises(StorageError, match="test error"): raise StorageError("test error") class TestFileNotFoundError: """Tests for FileNotFoundStorageError exception.""" def test_file_not_found_is_storage_error(self) -> None: """Test that FileNotFoundStorageError is a StorageError.""" from shared.storage.base import FileNotFoundStorageError, StorageError assert issubclass(FileNotFoundStorageError, StorageError) def test_file_not_found_with_path(self) -> None: """Test FileNotFoundStorageError with path.""" from shared.storage.base import FileNotFoundStorageError error = FileNotFoundStorageError("images/test.png") assert "images/test.png" in str(error) class TestStorageConfig: """Tests for StorageConfig dataclass.""" def test_storage_config_creation(self) -> None: """Test creating StorageConfig.""" from shared.storage.base import StorageConfig config = StorageConfig( backend_type="azure_blob", connection_string="DefaultEndpointsProtocol=https;...", container_name="training-images", ) assert config.backend_type == "azure_blob" assert config.connection_string == "DefaultEndpointsProtocol=https;..." assert config.container_name == "training-images" def test_storage_config_defaults(self) -> None: """Test StorageConfig with defaults.""" from shared.storage.base import StorageConfig config = StorageConfig(backend_type="local") assert config.backend_type == "local" assert config.connection_string is None assert config.container_name is None assert config.base_path is None def test_storage_config_with_base_path(self) -> None: """Test StorageConfig with base_path for local backend.""" from shared.storage.base import StorageConfig config = StorageConfig( backend_type="local", base_path=Path("/data/images"), ) assert config.backend_type == "local" assert config.base_path == Path("/data/images") def test_storage_config_immutable(self) -> None: """Test that StorageConfig is immutable (frozen).""" from shared.storage.base import StorageConfig config = StorageConfig(backend_type="local") with pytest.raises(AttributeError): config.backend_type = "azure_blob" # type: ignore