WIP
This commit is contained in:
301
tests/shared/storage/test_base.py
Normal file
301
tests/shared/storage/test_base.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user