Files
invoice-master-poc-v2/tests/shared/storage/test_presigned_urls.py
Yaojia Wang a516de4320 WIP
2026-02-01 00:08:40 +01:00

265 lines
9.9 KiB
Python

"""
Tests for pre-signed URL functionality across all storage backends.
TDD Phase 1: RED - Write tests first, then implement to pass.
"""
import shutil
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
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
class TestStorageBackendInterfacePresignedUrl:
"""Tests for get_presigned_url in StorageBackend interface."""
def test_subclass_must_implement_get_presigned_url(self) -> None:
"""Test that subclass must implement get_presigned_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
def get_url(self, remote_path: str) -> str:
return ""
with pytest.raises(TypeError):
IncompleteBackend() # type: ignore
def test_valid_subclass_with_get_presigned_url_can_be_instantiated(self) -> None:
"""Test that a complete subclass with get_presigned_url 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 f"https://example.com/{remote_path}?token=abc"
backend = CompleteBackend()
assert isinstance(backend, StorageBackend)
class TestLocalStorageBackendPresignedUrl:
"""Tests for LocalStorageBackend.get_presigned_url method."""
def test_get_presigned_url_returns_file_uri(
self, temp_storage_dir: Path, sample_file: Path
) -> None:
"""Test get_presigned_url returns file:// URI 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")
url = backend.get_presigned_url("sample.txt")
assert url.startswith("file://")
assert "sample.txt" in url
def test_get_presigned_url_with_custom_expiry(
self, temp_storage_dir: Path, sample_file: Path
) -> None:
"""Test get_presigned_url accepts expires_in_seconds parameter."""
from shared.storage.local import LocalStorageBackend
storage_dir = temp_storage_dir / "storage"
backend = LocalStorageBackend(base_path=storage_dir)
backend.upload(sample_file, "sample.txt")
# For local storage, expiry is ignored but should not raise error
url = backend.get_presigned_url("sample.txt", expires_in_seconds=7200)
assert url.startswith("file://")
def test_get_presigned_url_nonexistent_file_raises(
self, temp_storage_dir: Path
) -> None:
"""Test get_presigned_url raises FileNotFoundStorageError for missing 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_presigned_url("nonexistent.txt")
def test_get_presigned_url_path_traversal_blocked(
self, temp_storage_dir: Path
) -> None:
"""Test that path traversal in get_presigned_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_presigned_url("../escape.txt")
def test_get_presigned_url_nested_path(
self, temp_storage_dir: Path, sample_file: Path
) -> None:
"""Test get_presigned_url works with nested paths."""
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")
url = backend.get_presigned_url("a/b/c/sample.txt")
assert url.startswith("file://")
assert "sample.txt" in url
class TestAzureBlobStorageBackendPresignedUrl:
"""Tests for AzureBlobStorageBackend.get_presigned_url method."""
@patch("shared.storage.azure.BlobServiceClient")
def test_get_presigned_url_generates_sas_url(
self, mock_blob_service_class: MagicMock
) -> None:
"""Test get_presigned_url generates URL with SAS token."""
from shared.storage.azure import AzureBlobStorageBackend
# Setup mocks
mock_blob_service = MagicMock()
mock_blob_service.account_name = "testaccount"
mock_blob_service_class.from_connection_string.return_value = mock_blob_service
mock_container = MagicMock()
mock_container.exists.return_value = True
mock_blob_service.get_container_client.return_value = mock_container
mock_blob_client = MagicMock()
mock_blob_client.exists.return_value = True
mock_blob_client.url = "https://testaccount.blob.core.windows.net/container/test.txt"
mock_container.get_blob_client.return_value = mock_blob_client
backend = AzureBlobStorageBackend(
connection_string="DefaultEndpointsProtocol=https;AccountName=testaccount;AccountKey=testkey==;EndpointSuffix=core.windows.net",
container_name="container",
)
with patch("shared.storage.azure.generate_blob_sas") as mock_generate_sas:
mock_generate_sas.return_value = "sv=2021-06-08&sr=b&sig=abc123"
url = backend.get_presigned_url("test.txt", expires_in_seconds=3600)
assert "https://testaccount.blob.core.windows.net" in url
assert "sv=2021-06-08" in url or "test.txt" in url
@patch("shared.storage.azure.BlobServiceClient")
def test_get_presigned_url_nonexistent_blob_raises(
self, mock_blob_service_class: MagicMock
) -> None:
"""Test get_presigned_url raises for nonexistent blob."""
from shared.storage.base import FileNotFoundStorageError
from shared.storage.azure import AzureBlobStorageBackend
mock_blob_service = MagicMock()
mock_blob_service_class.from_connection_string.return_value = mock_blob_service
mock_container = MagicMock()
mock_container.exists.return_value = True
mock_blob_service.get_container_client.return_value = mock_container
mock_blob_client = MagicMock()
mock_blob_client.exists.return_value = False
mock_container.get_blob_client.return_value = mock_blob_client
backend = AzureBlobStorageBackend(
connection_string="DefaultEndpointsProtocol=https;AccountName=test;AccountKey=key==;EndpointSuffix=core.windows.net",
container_name="container",
)
with pytest.raises(FileNotFoundStorageError):
backend.get_presigned_url("nonexistent.txt")
@patch("shared.storage.azure.BlobServiceClient")
def test_get_presigned_url_uses_custom_expiry(
self, mock_blob_service_class: MagicMock
) -> None:
"""Test get_presigned_url uses custom expiry time."""
from shared.storage.azure import AzureBlobStorageBackend
mock_blob_service = MagicMock()
mock_blob_service.account_name = "testaccount"
mock_blob_service_class.from_connection_string.return_value = mock_blob_service
mock_container = MagicMock()
mock_container.exists.return_value = True
mock_blob_service.get_container_client.return_value = mock_container
mock_blob_client = MagicMock()
mock_blob_client.exists.return_value = True
mock_blob_client.url = "https://testaccount.blob.core.windows.net/container/test.txt"
mock_container.get_blob_client.return_value = mock_blob_client
backend = AzureBlobStorageBackend(
connection_string="DefaultEndpointsProtocol=https;AccountName=testaccount;AccountKey=testkey==;EndpointSuffix=core.windows.net",
container_name="container",
)
with patch("shared.storage.azure.generate_blob_sas") as mock_generate_sas:
mock_generate_sas.return_value = "sv=2021-06-08&sr=b&sig=abc123"
backend.get_presigned_url("test.txt", expires_in_seconds=7200)
# Verify generate_blob_sas was called (expiry is part of the call)
mock_generate_sas.assert_called_once()