Files
Yaojia Wang a516de4320 WIP
2026-02-01 00:08:40 +01:00

302 lines
9.8 KiB
Python

"""
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