223 lines
8.6 KiB
Python
223 lines
8.6 KiB
Python
"""
|
||
Тесты для helper_bot.utils.s3_storage (S3StorageService).
|
||
"""
|
||
|
||
import tempfile
|
||
from pathlib import Path
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from helper_bot.utils.s3_storage import S3StorageService
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestS3StorageServiceInit:
|
||
"""Тесты инициализации S3StorageService."""
|
||
|
||
def test_init_stores_params(self):
|
||
"""Параметры сохраняются в атрибутах."""
|
||
with patch("helper_bot.utils.s3_storage.aioboto3.Session"):
|
||
service = S3StorageService(
|
||
endpoint_url="http://s3",
|
||
access_key="ak",
|
||
secret_key="sk",
|
||
bucket_name="bucket",
|
||
region="eu-west-1",
|
||
)
|
||
assert service.endpoint_url == "http://s3"
|
||
assert service.bucket_name == "bucket"
|
||
assert service.region == "eu-west-1"
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestS3StorageServiceGenerateS3Key:
|
||
"""Тесты generate_s3_key."""
|
||
|
||
@pytest.fixture
|
||
def service(self):
|
||
"""Сервис без реального session."""
|
||
with patch("helper_bot.utils.s3_storage.aioboto3.Session"):
|
||
return S3StorageService(
|
||
endpoint_url="http://s3",
|
||
access_key="ak",
|
||
secret_key="sk",
|
||
bucket_name="b",
|
||
)
|
||
|
||
def test_photo_key(self, service):
|
||
"""Ключ для photo — photos/{id}.jpg."""
|
||
key = service.generate_s3_key("photo", "file_123")
|
||
assert key == "photos/file_123.jpg"
|
||
|
||
def test_video_key(self, service):
|
||
"""Ключ для video — videos/{id}.mp4."""
|
||
key = service.generate_s3_key("video", "vid_1")
|
||
assert key == "videos/vid_1.mp4"
|
||
|
||
def test_audio_key(self, service):
|
||
"""Ключ для audio — music/{id}.mp3."""
|
||
key = service.generate_s3_key("audio", "a1")
|
||
assert key == "music/a1.mp3"
|
||
|
||
def test_voice_key(self, service):
|
||
"""Ключ для voice — voice/{id}.ogg."""
|
||
key = service.generate_s3_key("voice", "v1")
|
||
assert key == "voice/v1.ogg"
|
||
|
||
def test_other_key(self, service):
|
||
"""Неизвестный тип — other/{id}.bin."""
|
||
key = service.generate_s3_key("other", "x")
|
||
assert key == "other/x.bin"
|
||
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
class TestS3StorageServiceUploadDownload:
|
||
"""Тесты upload_file, download_file, file_exists, delete_file через мок session.client."""
|
||
|
||
@pytest.fixture
|
||
def service(self):
|
||
"""Сервис с замоканной session."""
|
||
mock_session = MagicMock()
|
||
mock_context = AsyncMock()
|
||
mock_s3 = MagicMock()
|
||
mock_s3.upload_file = AsyncMock()
|
||
mock_s3.upload_fileobj = AsyncMock()
|
||
mock_s3.download_file = AsyncMock()
|
||
mock_s3.head_object = AsyncMock()
|
||
mock_s3.delete_object = AsyncMock()
|
||
mock_context.__aenter__.return_value = mock_s3
|
||
mock_context.__aexit__.return_value = None
|
||
mock_session.client.return_value = mock_context
|
||
with patch(
|
||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||
):
|
||
s = S3StorageService(
|
||
endpoint_url="http://s3",
|
||
access_key="ak",
|
||
secret_key="sk",
|
||
bucket_name="bucket",
|
||
)
|
||
s._mock_s3 = mock_s3
|
||
return s
|
||
|
||
async def test_upload_file_success(self, service):
|
||
"""upload_file при успехе возвращает True."""
|
||
result = await service.upload_file("/tmp/f", "key")
|
||
assert result is True
|
||
service._mock_s3.upload_file.assert_called_once()
|
||
|
||
async def test_upload_file_with_content_type(self, service):
|
||
"""upload_file с content_type передаёт ExtraArgs."""
|
||
await service.upload_file("/tmp/f", "key", content_type="image/jpeg")
|
||
call_kwargs = service._mock_s3.upload_file.call_args[1]
|
||
assert call_kwargs.get("ExtraArgs", {}).get("ContentType") == "image/jpeg"
|
||
|
||
async def test_upload_file_exception_returns_false(self, service):
|
||
"""При исключении upload_file возвращает False."""
|
||
service._mock_s3.upload_file = AsyncMock(side_effect=Exception("network error"))
|
||
result = await service.upload_file("/tmp/f", "key")
|
||
assert result is False
|
||
|
||
async def test_download_file_success(self, service):
|
||
"""download_file при успехе возвращает True."""
|
||
with patch("os.makedirs"):
|
||
result = await service.download_file("key", "/tmp/out")
|
||
assert result is True
|
||
service._mock_s3.download_file.assert_called_once()
|
||
|
||
async def test_download_file_exception_returns_false(self, service):
|
||
"""При исключении download_file возвращает False."""
|
||
service._mock_s3.download_file = AsyncMock(side_effect=Exception("error"))
|
||
with patch("os.makedirs"):
|
||
result = await service.download_file("key", "/tmp/out")
|
||
assert result is False
|
||
|
||
async def test_upload_fileobj_success(self, service):
|
||
"""upload_fileobj при успехе возвращает True."""
|
||
f = MagicMock()
|
||
result = await service.upload_fileobj(f, "key")
|
||
assert result is True
|
||
service._mock_s3.upload_fileobj.assert_called_once()
|
||
|
||
async def test_file_exists_true(self, service):
|
||
"""file_exists при успешном head_object возвращает True."""
|
||
result = await service.file_exists("key")
|
||
assert result is True
|
||
|
||
async def test_file_exists_false_on_exception(self, service):
|
||
"""file_exists при исключении возвращает False."""
|
||
service._mock_s3.head_object = AsyncMock(side_effect=Exception())
|
||
result = await service.file_exists("key")
|
||
assert result is False
|
||
|
||
async def test_delete_file_success(self, service):
|
||
"""delete_file при успехе возвращает True."""
|
||
result = await service.delete_file("key")
|
||
assert result is True
|
||
service._mock_s3.delete_object.assert_called_once_with(
|
||
Bucket="bucket", Key="key"
|
||
)
|
||
|
||
async def test_delete_file_exception_returns_false(self, service):
|
||
"""При исключении delete_file возвращает False."""
|
||
service._mock_s3.delete_object = AsyncMock(side_effect=Exception())
|
||
result = await service.delete_file("key")
|
||
assert result is False
|
||
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
class TestS3StorageServiceDownloadToTemp:
|
||
"""Тесты download_to_temp."""
|
||
|
||
async def test_download_to_temp_success_returns_path(self):
|
||
"""При успешном download возвращается путь к временному файлу."""
|
||
mock_session = MagicMock()
|
||
mock_context = AsyncMock()
|
||
mock_s3 = MagicMock()
|
||
mock_s3.download_file = AsyncMock()
|
||
mock_context.__aenter__.return_value = mock_s3
|
||
mock_context.__aexit__.return_value = None
|
||
mock_session.client.return_value = mock_context
|
||
with patch(
|
||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||
):
|
||
service = S3StorageService(
|
||
endpoint_url="http://s3",
|
||
access_key="ak",
|
||
secret_key="sk",
|
||
bucket_name="b",
|
||
)
|
||
with patch("os.makedirs"):
|
||
path = await service.download_to_temp("photos/1.jpg")
|
||
if path:
|
||
assert Path(path).suffix in (".jpg", "")
|
||
try:
|
||
Path(path).unlink(missing_ok=True)
|
||
except Exception:
|
||
pass
|
||
|
||
async def test_download_to_temp_failure_returns_none(self):
|
||
"""При неуспешном download возвращается None."""
|
||
mock_session = MagicMock()
|
||
mock_context = AsyncMock()
|
||
mock_s3 = MagicMock()
|
||
mock_s3.download_file = AsyncMock()
|
||
mock_context.__aenter__.return_value = mock_s3
|
||
mock_context.__aexit__.return_value = None
|
||
mock_session.client.return_value = mock_context
|
||
with patch(
|
||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||
):
|
||
service = S3StorageService(
|
||
endpoint_url="http://s3",
|
||
access_key="ak",
|
||
secret_key="sk",
|
||
bucket_name="b",
|
||
)
|
||
with patch.object(service, "download_file", AsyncMock(return_value=False)):
|
||
path = await service.download_to_temp("key")
|
||
assert path is None
|