Spaces:
Sleeping
Sleeping
| """ | |
| Rigorous Tests for Gemini AI Service. | |
| Tests cover: | |
| 1. Initialization & API key handling | |
| 2. Concurrency semaphores | |
| 3. Text generation | |
| 4. Animation prompt generation | |
| 5. Image analysis & editing | |
| 6. Video generation, status checking, downloading | |
| 7. Error handling | |
| """ | |
| import pytest | |
| import asyncio | |
| import os | |
| import tempfile | |
| from unittest.mock import patch, MagicMock, AsyncMock, PropertyMock | |
| from datetime import datetime | |
| # ============================================================================= | |
| # 1. Initialization & Configuration Tests | |
| # ============================================================================= | |
| class TestGeminiServiceInit: | |
| """Test GeminiService initialization and configuration.""" | |
| def test_init_with_explicit_api_key(self): | |
| """Service initializes with explicit API key.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key-123") | |
| assert service.api_key == "test-key-123" | |
| mock_genai.Client.assert_called_once_with(api_key="test-key-123") | |
| def test_init_with_env_fallback(self): | |
| """Service falls back to environment variable for API key.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch.dict(os.environ, {"GEMINI_API_KEY": "env-key-456"}): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService() | |
| assert service.api_key == "env-key-456" | |
| def test_init_fails_without_api_key(self): | |
| """Service raises error when no API key available.""" | |
| with patch.dict(os.environ, {}, clear=True): | |
| # Remove GEMINI_API_KEY if present | |
| os.environ.pop("GEMINI_API_KEY", None) | |
| os.environ.pop("GEMINI_API_KEYS", None) | |
| from services.gemini_service import get_gemini_api_key | |
| with pytest.raises(ValueError, match="Server Authentication Error"): | |
| get_gemini_api_key() | |
| def test_models_dict_has_required_entries(self): | |
| """MODELS dictionary has all required model names.""" | |
| from services.gemini_service import MODELS | |
| assert "text_generation" in MODELS | |
| assert "image_edit" in MODELS | |
| assert "video_generation" in MODELS | |
| assert all(isinstance(v, str) for v in MODELS.values()) | |
| # ============================================================================= | |
| # 2. Semaphore Concurrency Tests | |
| # ============================================================================= | |
| class TestSemaphoreConcurrency: | |
| """Test concurrency control via semaphores.""" | |
| def test_video_semaphore_respects_limit(self): | |
| """Video semaphore uses MAX_CONCURRENT_VIDEOS.""" | |
| # Reset global | |
| import services.gemini_service as gs | |
| gs._video_semaphore = None | |
| with patch.object(gs, 'MAX_CONCURRENT_VIDEOS', 3): | |
| gs._video_semaphore = None # Reset | |
| sem = gs.get_video_semaphore() | |
| # Semaphore internal value | |
| assert sem._value == 3 | |
| def test_image_semaphore_respects_limit(self): | |
| """Image semaphore uses MAX_CONCURRENT_IMAGES.""" | |
| import services.gemini_service as gs | |
| gs._image_semaphore = None | |
| with patch.object(gs, 'MAX_CONCURRENT_IMAGES', 5): | |
| gs._image_semaphore = None | |
| sem = gs.get_image_semaphore() | |
| assert sem._value == 5 | |
| def test_text_semaphore_respects_limit(self): | |
| """Text semaphore uses MAX_CONCURRENT_TEXT.""" | |
| import services.gemini_service as gs | |
| gs._text_semaphore = None | |
| with patch.object(gs, 'MAX_CONCURRENT_TEXT', 10): | |
| gs._text_semaphore = None | |
| sem = gs.get_text_semaphore() | |
| assert sem._value == 10 | |
| def test_semaphores_are_singletons(self): | |
| """Calling get_*_semaphore multiple times returns same object.""" | |
| import services.gemini_service as gs | |
| gs._video_semaphore = None | |
| gs._image_semaphore = None | |
| gs._text_semaphore = None | |
| video1 = gs.get_video_semaphore() | |
| video2 = gs.get_video_semaphore() | |
| assert video1 is video2 | |
| image1 = gs.get_image_semaphore() | |
| image2 = gs.get_image_semaphore() | |
| assert image1 is image2 | |
| text1 = gs.get_text_semaphore() | |
| text2 = gs.get_text_semaphore() | |
| assert text1 is text2 | |
| # ============================================================================= | |
| # 3. Text Generation Tests | |
| # ============================================================================= | |
| class TestTextGeneration: | |
| """Test generate_text method.""" | |
| async def test_generate_text_success(self): | |
| """generate_text returns text on success.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| # Mock response | |
| mock_response = MagicMock() | |
| mock_response.text = "Generated text response" | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_text("Hello world") | |
| assert result == "Generated text response" | |
| async def test_generate_text_with_custom_model(self): | |
| """generate_text uses custom model when provided.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = "Custom model response" | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_text("Hello", model="custom-model") | |
| # Verify custom model was used | |
| call_args = mock_genai.Client.return_value.models.generate_content.call_args | |
| assert call_args.kwargs.get('model') == "custom-model" | |
| async def test_generate_text_empty_response(self): | |
| """generate_text returns empty string for None response.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = None | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_text("Hello") | |
| assert result == "" | |
| async def test_generate_text_api_error_404(self): | |
| """generate_text raises ValueError for 404 error.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_genai.Client.return_value.models.generate_content.side_effect = Exception("404 NOT_FOUND") | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Model not found"): | |
| await service.generate_text("Hello") | |
| # ============================================================================= | |
| # 4. Animation Prompt Tests | |
| # ============================================================================= | |
| class TestAnimationPrompt: | |
| """Test generate_animation_prompt method.""" | |
| async def test_generate_animation_prompt_default(self): | |
| """generate_animation_prompt uses default prompt.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = "Subtle zoom with camera pan" | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_animation_prompt( | |
| base64_image="base64data", | |
| mime_type="image/jpeg" | |
| ) | |
| assert result == "Subtle zoom with camera pan" | |
| async def test_generate_animation_prompt_custom(self): | |
| """generate_animation_prompt uses custom prompt when provided.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = "Custom animation" | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_animation_prompt( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| custom_prompt="Make it dramatic" | |
| ) | |
| assert result == "Custom animation" | |
| async def test_generate_animation_prompt_fallback(self): | |
| """generate_animation_prompt returns fallback on empty response.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = None | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.generate_animation_prompt( | |
| base64_image="base64data", | |
| mime_type="image/jpeg" | |
| ) | |
| assert result == "Cinematic subtle movement" | |
| # ============================================================================= | |
| # 5. Image Analysis Tests | |
| # ============================================================================= | |
| class TestImageAnalysis: | |
| """Test analyze_image method.""" | |
| async def test_analyze_image_success(self): | |
| """analyze_image returns analysis text.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = "This image shows a sunset over mountains" | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.analyze_image( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| prompt="Describe this image" | |
| ) | |
| assert result == "This image shows a sunset over mountains" | |
| async def test_analyze_image_empty_response(self): | |
| """analyze_image returns empty string for None response.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.text = None | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.analyze_image( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| prompt="Describe" | |
| ) | |
| assert result == "" | |
| # ============================================================================= | |
| # 6. Image Editing Tests | |
| # ============================================================================= | |
| class TestImageEditing: | |
| """Test edit_image method.""" | |
| async def test_edit_image_returns_data_uri(self): | |
| """edit_image returns base64 data URI.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| # Create mock response structure | |
| mock_inline_data = MagicMock() | |
| mock_inline_data.data = "base64imagedata" | |
| mock_inline_data.mime_type = "image/png" | |
| mock_part = MagicMock() | |
| mock_part.inline_data = mock_inline_data | |
| mock_content = MagicMock() | |
| mock_content.parts = [mock_part] | |
| mock_candidate = MagicMock() | |
| mock_candidate.content = mock_content | |
| mock_response = MagicMock() | |
| mock_response.candidates = [mock_candidate] | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.edit_image( | |
| base64_image="input-base64", | |
| mime_type="image/jpeg", | |
| prompt="Make it colorful" | |
| ) | |
| assert result == "data:image/png;base64,base64imagedata" | |
| async def test_edit_image_no_candidates(self): | |
| """edit_image raises error when no candidates returned.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_response = MagicMock() | |
| mock_response.candidates = [] | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="No candidates returned"): | |
| await service.edit_image( | |
| base64_image="input-base64", | |
| mime_type="image/jpeg", | |
| prompt="Edit" | |
| ) | |
| async def test_edit_image_no_image_data(self): | |
| """edit_image raises error when no image data in parts.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| # Part without inline_data | |
| mock_part = MagicMock() | |
| mock_part.inline_data = None | |
| mock_content = MagicMock() | |
| mock_content.parts = [mock_part] | |
| mock_candidate = MagicMock() | |
| mock_candidate.content = mock_content | |
| mock_response = MagicMock() | |
| mock_response.candidates = [mock_candidate] | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="No image data found"): | |
| await service.edit_image( | |
| base64_image="input-base64", | |
| mime_type="image/jpeg", | |
| prompt="Edit" | |
| ) | |
| async def test_edit_image_default_prompt(self): | |
| """edit_image uses default prompt when empty.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_inline_data = MagicMock() | |
| mock_inline_data.data = "base64data" | |
| mock_inline_data.mime_type = "image/png" | |
| mock_part = MagicMock() | |
| mock_part.inline_data = mock_inline_data | |
| mock_content = MagicMock() | |
| mock_content.parts = [mock_part] | |
| mock_candidate = MagicMock() | |
| mock_candidate.content = mock_content | |
| mock_response = MagicMock() | |
| mock_response.candidates = [mock_candidate] | |
| mock_genai.Client.return_value.models.generate_content.return_value = mock_response | |
| service = GeminiService(api_key="test-key") | |
| result = await service.edit_image( | |
| base64_image="input", | |
| mime_type="image/jpeg", | |
| prompt="" # Empty prompt | |
| ) | |
| assert "data:" in result | |
| # ============================================================================= | |
| # 7. Video Generation Tests | |
| # ============================================================================= | |
| class TestVideoGeneration: | |
| """Test start_video_generation method.""" | |
| async def test_start_video_returns_operation_dict(self): | |
| """start_video_generation returns operation dictionary.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_operation = MagicMock() | |
| mock_operation.name = "operations/video-123" | |
| mock_operation.done = False | |
| mock_genai.Client.return_value.models.generate_videos.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| result = await service.start_video_generation( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| prompt="Animate this" | |
| ) | |
| assert result["gemini_operation_name"] == "operations/video-123" | |
| assert result["done"] == False | |
| assert result["status"] == "pending" | |
| async def test_start_video_completed_immediately(self): | |
| """start_video_generation returns completed when done=True.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_operation = MagicMock() | |
| mock_operation.name = "operations/video-123" | |
| mock_operation.done = True | |
| mock_genai.Client.return_value.models.generate_videos.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| result = await service.start_video_generation( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| prompt="Animate this" | |
| ) | |
| assert result["status"] == "completed" | |
| async def test_start_video_with_params(self): | |
| """start_video_generation passes aspect_ratio and resolution.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| with patch('services.gemini_service.types'): | |
| from services.gemini_service import GeminiService | |
| mock_operation = MagicMock() | |
| mock_operation.name = "operations/video-123" | |
| mock_operation.done = False | |
| mock_genai.Client.return_value.models.generate_videos.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| await service.start_video_generation( | |
| base64_image="base64data", | |
| mime_type="image/jpeg", | |
| prompt="Animate", | |
| aspect_ratio="9:16", | |
| resolution="1080p", | |
| number_of_videos=2 | |
| ) | |
| # Verify config was passed | |
| call_args = mock_genai.Client.return_value.models.generate_videos.call_args | |
| assert call_args is not None | |
| # ============================================================================= | |
| # 8. Video Status Checking Tests | |
| # ============================================================================= | |
| class TestVideoStatusChecking: | |
| """Test check_video_status method.""" | |
| async def test_check_status_pending(self): | |
| """check_video_status returns pending when not done.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_operation = MagicMock() | |
| mock_operation.done = False | |
| mock_operation.error = None | |
| mock_genai.Client.return_value.operations.get.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| result = await service.check_video_status("operations/video-123") | |
| assert result["done"] == False | |
| assert result["status"] == "pending" | |
| async def test_check_status_completed_with_url(self): | |
| """check_video_status returns completed with video URL.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| # Build nested mock structure | |
| mock_video = MagicMock() | |
| mock_video.uri = "https://storage.googleapis.com/video.mp4" | |
| mock_generated_video = MagicMock() | |
| mock_generated_video.video = mock_video | |
| mock_result = MagicMock() | |
| mock_result.generated_videos = [mock_generated_video] | |
| mock_operation = MagicMock() | |
| mock_operation.done = True | |
| mock_operation.error = None | |
| mock_operation.result = mock_result | |
| mock_genai.Client.return_value.operations.get.return_value = mock_operation | |
| service = GeminiService(api_key="test-api-key") | |
| result = await service.check_video_status("operations/video-123") | |
| assert result["done"] == True | |
| assert result["status"] == "completed" | |
| assert "video_url" in result | |
| assert "test-api-key" in result["video_url"] # API key appended | |
| async def test_check_status_operation_error(self): | |
| """check_video_status returns failed on operation error.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_error = MagicMock() | |
| mock_error.message = "Content blocked by policy" | |
| mock_operation = MagicMock() | |
| mock_operation.done = True | |
| mock_operation.error = mock_error | |
| mock_genai.Client.return_value.operations.get.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| result = await service.check_video_status("operations/video-123") | |
| assert result["done"] == True | |
| assert result["status"] == "failed" | |
| assert "error" in result | |
| async def test_check_status_404_expired(self): | |
| """check_video_status handles 404 for expired operation.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_genai.Client.return_value.operations.get.side_effect = Exception("404 NOT_FOUND") | |
| service = GeminiService(api_key="test-key") | |
| result = await service.check_video_status("operations/expired-123") | |
| assert result["done"] == True | |
| assert result["status"] == "failed" | |
| assert "expired" in result["error"].lower() | |
| async def test_check_status_no_video_uri(self): | |
| """check_video_status returns failed when no video URI.""" | |
| with patch('services.gemini_service.genai') as mock_genai: | |
| from services.gemini_service import GeminiService | |
| mock_result = MagicMock() | |
| mock_result.generated_videos = [] # Empty | |
| mock_operation = MagicMock() | |
| mock_operation.done = True | |
| mock_operation.error = None | |
| mock_operation.result = mock_result | |
| mock_genai.Client.return_value.operations.get.return_value = mock_operation | |
| service = GeminiService(api_key="test-key") | |
| result = await service.check_video_status("operations/video-123") | |
| assert result["status"] == "failed" | |
| assert "safety filters" in result["error"].lower() | |
| # ============================================================================= | |
| # 9. Video Download Tests | |
| # ============================================================================= | |
| class TestVideoDownload: | |
| """Test download_video method.""" | |
| async def test_download_video_saves_file(self): | |
| """download_video saves file and returns filename.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService, DOWNLOADS_DIR | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_response = MagicMock() | |
| mock_response.content = b"fake video data" | |
| mock_response.raise_for_status = MagicMock() | |
| mock_client_instance = AsyncMock() | |
| mock_client_instance.get.return_value = mock_response | |
| mock_client_instance.__aenter__.return_value = mock_client_instance | |
| mock_client_instance.__aexit__.return_value = None | |
| mock_client.return_value = mock_client_instance | |
| service = GeminiService(api_key="test-key") | |
| # Use temp directory | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| with patch.object( | |
| __import__('services.gemini_service', fromlist=['DOWNLOADS_DIR']), | |
| 'DOWNLOADS_DIR', | |
| temp_dir | |
| ): | |
| result = await service.download_video( | |
| "https://example.com/video.mp4", | |
| "test-op-123" | |
| ) | |
| assert result == "test-op-123.mp4" | |
| async def test_download_video_http_error(self): | |
| """download_video raises error on HTTP failure.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client_instance = AsyncMock() | |
| mock_client_instance.get.side_effect = Exception("Connection refused") | |
| mock_client_instance.__aenter__.return_value = mock_client_instance | |
| mock_client_instance.__aexit__.return_value = None | |
| mock_client.return_value = mock_client_instance | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Failed to download"): | |
| await service.download_video( | |
| "https://example.com/video.mp4", | |
| "test-op-123" | |
| ) | |
| async def test_download_video_follows_redirects(self): | |
| """download_video client is configured to follow redirects.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_response = MagicMock() | |
| mock_response.content = b"video data" | |
| mock_response.raise_for_status = MagicMock() | |
| mock_client_instance = AsyncMock() | |
| mock_client_instance.get.return_value = mock_response | |
| mock_client_instance.__aenter__.return_value = mock_client_instance | |
| mock_client_instance.__aexit__.return_value = None | |
| mock_client.return_value = mock_client_instance | |
| service = GeminiService(api_key="test-key") | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| with patch('services.gemini_service.DOWNLOADS_DIR', temp_dir): | |
| await service.download_video( | |
| "https://example.com/video.mp4", | |
| "redirect-test" | |
| ) | |
| # Verify follow_redirects=True was passed | |
| mock_client.assert_called_with(timeout=120.0, follow_redirects=True) | |
| # ============================================================================= | |
| # 10. Error Handling Tests | |
| # ============================================================================= | |
| class TestErrorHandling: | |
| """Test _handle_api_error method.""" | |
| def test_handle_api_error_404(self): | |
| """_handle_api_error raises ValueError for 404.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Model not found"): | |
| service._handle_api_error(Exception("Error 404"), "test-model") | |
| def test_handle_api_error_not_found(self): | |
| """_handle_api_error handles NOT_FOUND in message.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Model not found"): | |
| service._handle_api_error(Exception("NOT_FOUND: resource"), "test-model") | |
| def test_handle_api_error_entity_not_found(self): | |
| """_handle_api_error handles 'Requested entity was not found'.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Model not found"): | |
| service._handle_api_error( | |
| Exception("Requested entity was not found"), | |
| "test-model" | |
| ) | |
| def test_handle_api_error_bracket_5_pattern(self): | |
| """_handle_api_error handles [5, pattern.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(ValueError, match="Model not found"): | |
| service._handle_api_error( | |
| Exception("Response [5, 'NOT_FOUND']"), | |
| "test-model" | |
| ) | |
| def test_handle_api_error_reraises_other(self): | |
| """_handle_api_error re-raises non-404 errors.""" | |
| with patch('services.gemini_service.genai'): | |
| from services.gemini_service import GeminiService | |
| service = GeminiService(api_key="test-key") | |
| with pytest.raises(RuntimeError, match="Connection timeout"): | |
| service._handle_api_error( | |
| RuntimeError("Connection timeout"), | |
| "test-model" | |
| ) | |
| # ============================================================================= | |
| # 11. Downloads Directory Tests | |
| # ============================================================================= | |
| class TestDownloadsDirectory: | |
| """Test downloads directory handling.""" | |
| def test_downloads_dir_exists(self): | |
| """DOWNLOADS_DIR is created on module import.""" | |
| from services.gemini_service import DOWNLOADS_DIR | |
| assert os.path.exists(DOWNLOADS_DIR) | |
| assert os.path.isdir(DOWNLOADS_DIR) | |
| def test_downloads_dir_is_in_project(self): | |
| """DOWNLOADS_DIR is within project directory.""" | |
| from services.gemini_service import DOWNLOADS_DIR | |
| assert "downloads" in DOWNLOADS_DIR | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |