""" Тесты для 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