""" Unit tests for AI document generation service """ import pytest from unittest.mock import AsyncMock, Mock, patch import asyncio from src.backend.services.ai_service import AIService, ai_service class TestAIService: """Test AI Service functionality.""" def test_ai_service_initialization(self): """Test AI service initializes correctly.""" service = AIService() # Without API keys, clients should be None assert service.claude_client is None assert service.openai_client is None @patch('src.backend.services.ai_service.settings') def test_ai_service_with_claude_key(self, mock_settings): """Test AI service initialization with Claude API key.""" mock_settings.claude_api_key = "test-claude-key" mock_settings.openai_api_key = None with patch('src.backend.services.ai_service.anthropic.Anthropic') as mock_anthropic: service = AIService() mock_anthropic.assert_called_once_with(api_key="test-claude-key") assert service.claude_client is not None assert service.openai_client is None @patch('src.backend.services.ai_service.settings') def test_ai_service_with_openai_key(self, mock_settings): """Test AI service initialization with OpenAI API key.""" mock_settings.claude_api_key = None mock_settings.openai_api_key = "test-openai-key" with patch('src.backend.services.ai_service.openai.AsyncOpenAI') as mock_openai: service = AIService() mock_openai.assert_called_once_with(api_key="test-openai-key") assert service.claude_client is None assert service.openai_client is not None @pytest.mark.asyncio async def test_generate_cover_letter_template_fallback(self): """Test cover letter generation with template fallback.""" service = AIService() result = await service.generate_cover_letter( job_description="Python developer position requiring FastAPI skills", company_name="Tech Corp", role_title="Senior Python Developer", user_name="John Doe" ) assert "content" in result assert "model_used" in result assert "prompt" in result assert result["model_used"] == "template" assert "Tech Corp" in result["content"] assert "Senior Python Developer" in result["content"] assert "John Doe" in result["content"] assert "Dear Hiring Manager" in result["content"] @pytest.mark.asyncio async def test_generate_cover_letter_with_claude(self): """Test cover letter generation with Claude API.""" service = AIService() # Mock Claude client mock_claude = Mock() mock_response = Mock() mock_response.content = [Mock(text="Generated cover letter content")] mock_claude.messages.create.return_value = mock_response service.claude_client = mock_claude result = await service.generate_cover_letter( job_description="Python developer position", company_name="Test Company", role_title="Developer", user_name="Test User" ) assert result["content"] == "Generated cover letter content" assert result["model_used"] == "claude-3-haiku" assert "prompt" in result # Verify Claude API was called correctly mock_claude.messages.create.assert_called_once() call_args = mock_claude.messages.create.call_args assert call_args[1]["model"] == "claude-3-haiku-20240307" assert call_args[1]["max_tokens"] == 1000 @pytest.mark.asyncio async def test_generate_cover_letter_with_openai(self): """Test cover letter generation with OpenAI API.""" service = AIService() # Mock OpenAI client mock_openai = AsyncMock() mock_response = Mock() mock_response.choices = [Mock(message=Mock(content="OpenAI generated content"))] mock_openai.chat.completions.create.return_value = mock_response service.openai_client = mock_openai result = await service.generate_cover_letter( job_description="Software engineer role", company_name="OpenAI Corp", role_title="Engineer", user_name="AI User" ) assert result["content"] == "OpenAI generated content" assert result["model_used"] == "gpt-3.5-turbo" assert "prompt" in result # Verify OpenAI API was called correctly mock_openai.chat.completions.create.assert_called_once() call_args = mock_openai.chat.completions.create.call_args assert call_args[1]["model"] == "gpt-3.5-turbo" assert call_args[1]["max_tokens"] == 1000 @pytest.mark.asyncio async def test_generate_cover_letter_with_user_resume(self): """Test cover letter generation with user resume included.""" service = AIService() result = await service.generate_cover_letter( job_description="Python developer position", company_name="Resume Corp", role_title="Developer", user_name="Resume User", user_resume="John Doe\nSoftware Engineer\n5 years Python experience" ) # Should include resume information in prompt assert "Resume/Background" in result["prompt"] assert result["model_used"] == "template" @pytest.mark.asyncio async def test_generate_resume_optimization_template(self): """Test resume optimization with template fallback.""" service = AIService() current_resume = "John Smith\nDeveloper\n\nExperience:\n- 3 years Python\n- Web development" result = await service.generate_resume_optimization( current_resume=current_resume, job_description="Senior Python Developer requiring FastAPI", role_title="Senior Python Developer" ) assert "content" in result assert "model_used" in result assert "prompt" in result assert result["model_used"] == "template" assert "Senior Python Developer" in result["content"] assert current_resume in result["content"] @pytest.mark.asyncio async def test_generate_resume_optimization_with_ai_error(self): """Test resume optimization when AI service fails.""" service = AIService() # Mock Claude client that raises an exception mock_claude = Mock() mock_claude.messages.create.side_effect = Exception("API Error") service.claude_client = mock_claude result = await service.generate_resume_optimization( current_resume="Test resume", job_description="Test job", role_title="Test role" ) # Should fallback to template assert result["model_used"] == "template-fallback" assert "Test resume" in result["content"] def test_template_cover_letter_generation(self): """Test template cover letter generation.""" service = AIService() content = service._generate_template_cover_letter( company_name="Template Corp", role_title="Template Role", user_name="Template User", job_description="Python, JavaScript, React, SQL, AWS, Docker experience required" ) assert "Template Corp" in content assert "Template Role" in content assert "Template User" in content assert "Dear Hiring Manager" in content # Should extract and include relevant skills assert "Python" in content or "Javascript" in content def test_template_cover_letter_no_matching_skills(self): """Test template cover letter when no skills match.""" service = AIService() content = service._generate_template_cover_letter( company_name="No Skills Corp", role_title="Mysterious Role", user_name="Skill-less User", job_description="Experience with proprietary technology XYZ required" ) assert "No Skills Corp" in content assert "Mysterious Role" in content assert "Skill-less User" in content # Should not include skill text when no matches assert "with expertise in" not in content class TestAIServiceIntegration: """Test AI service integration and edge cases.""" @pytest.mark.asyncio async def test_concurrent_cover_letter_generation(self): """Test concurrent cover letter generation requests.""" service = AIService() # Create multiple concurrent requests tasks = [ service.generate_cover_letter( job_description=f"Job {i} description", company_name=f"Company {i}", role_title=f"Role {i}", user_name=f"User {i}" ) for i in range(5) ] results = await asyncio.gather(*tasks) # All should complete successfully assert len(results) == 5 for i, result in enumerate(results): assert f"Company {i}" in result["content"] assert f"Role {i}" in result["content"] assert result["model_used"] == "template" @pytest.mark.asyncio async def test_cover_letter_with_empty_inputs(self): """Test cover letter generation with empty inputs.""" service = AIService() result = await service.generate_cover_letter( job_description="", company_name="", role_title="", user_name="" ) # Should handle empty inputs gracefully assert "content" in result assert result["model_used"] == "template" @pytest.mark.asyncio async def test_cover_letter_with_very_long_inputs(self): """Test cover letter generation with very long inputs.""" service = AIService() long_description = "A" * 10000 # Very long job description result = await service.generate_cover_letter( job_description=long_description, company_name="Long Corp", role_title="Long Role", user_name="Long User" ) # Should handle long inputs assert "content" in result assert result["model_used"] == "template" @pytest.mark.asyncio async def test_resume_optimization_with_special_characters(self): """Test resume optimization with special characters.""" service = AIService() resume_with_special_chars = """ José González Software Engineer Experience: • 5 years of Python development • Expertise in FastAPI & PostgreSQL • Led team of 10+ developers """ result = await service.generate_resume_optimization( current_resume=resume_with_special_chars, job_description="Senior role requiring team leadership", role_title="Senior Developer" ) assert "content" in result assert "José González" in result["content"] assert result["model_used"] == "template" class TestAIServiceConfiguration: """Test AI service configuration and settings.""" @patch('src.backend.services.ai_service.settings') def test_ai_service_singleton(self, mock_settings): """Test that ai_service is a singleton instance.""" # The ai_service should be the same instance from src.backend.services.ai_service import ai_service as service1 from src.backend.services.ai_service import ai_service as service2 assert service1 is service2 @pytest.mark.asyncio async def test_error_handling_in_ai_generation(self): """Test error handling in AI generation methods.""" service = AIService() # Mock a client that raises an exception service.claude_client = Mock() service.claude_client.messages.create.side_effect = Exception("Network error") result = await service.generate_cover_letter( job_description="Test job", company_name="Error Corp", role_title="Error Role", user_name="Error User" ) # Should fallback gracefully assert result["model_used"] == "template-fallback" assert "Error Corp" in result["content"] def test_prompt_construction(self): """Test that prompts are constructed correctly.""" service = AIService() # This is tested indirectly through the template generation content = service._generate_template_cover_letter( company_name="Prompt Corp", role_title="Prompt Engineer", user_name="Prompt User", job_description="Looking for someone with strong prompting skills" ) assert "Prompt Corp" in content assert "Prompt Engineer" in content assert "Prompt User" in content @pytest.mark.integration class TestAIServiceWithRealAPIs: """Integration tests for AI service with real APIs (requires API keys).""" @pytest.mark.skipif( not hasattr(ai_service, 'claude_client') or ai_service.claude_client is None, reason="Claude API key not configured" ) @pytest.mark.asyncio async def test_real_claude_api_call(self): """Test actual Claude API call (only runs if API key is configured).""" result = await ai_service.generate_cover_letter( job_description="Python developer position with FastAPI", company_name="Real API Corp", role_title="Python Developer", user_name="Integration Test User" ) assert result["model_used"] == "claude-3-haiku" assert len(result["content"]) > 100 # Should be substantial content assert "Real API Corp" in result["content"] @pytest.mark.skipif( not hasattr(ai_service, 'openai_client') or ai_service.openai_client is None, reason="OpenAI API key not configured" ) @pytest.mark.asyncio async def test_real_openai_api_call(self): """Test actual OpenAI API call (only runs if API key is configured).""" # Temporarily disable Claude to force OpenAI usage original_claude = ai_service.claude_client ai_service.claude_client = None try: result = await ai_service.generate_cover_letter( job_description="Software engineer role requiring Python", company_name="OpenAI Test Corp", role_title="Software Engineer", user_name="OpenAI Test User" ) assert result["model_used"] == "gpt-3.5-turbo" assert len(result["content"]) > 100 assert "OpenAI Test Corp" in result["content"] finally: ai_service.claude_client = original_claude