455 lines
16 KiB
Python
455 lines
16 KiB
Python
"""
|
|
Integration tests for AI API endpoints
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
from src.backend.main import app
|
|
|
|
|
|
class TestAIDocumentEndpoints:
|
|
"""Test AI document generation API endpoints."""
|
|
|
|
def test_generate_cover_letter_success(self, test_client, test_user_token):
|
|
"""Test successful cover letter generation."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "We are looking for a Senior Python Developer with FastAPI experience and PostgreSQL knowledge. The ideal candidate will have 5+ years of experience.",
|
|
"company_name": "TechCorp Industries",
|
|
"role_title": "Senior Python Developer"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "content" in data
|
|
assert "model_used" in data
|
|
assert "generation_prompt" in data
|
|
|
|
# Verify content includes relevant information
|
|
assert "TechCorp Industries" in data["content"]
|
|
assert "Senior Python Developer" in data["content"]
|
|
assert len(data["content"]) > 100 # Should be substantial
|
|
|
|
# Should use template fallback without API keys
|
|
assert data["model_used"] == "template"
|
|
|
|
def test_generate_cover_letter_with_resume(self, test_client, test_user_token):
|
|
"""Test cover letter generation with user resume included."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Python developer role requiring Django experience",
|
|
"company_name": "Resume Corp",
|
|
"role_title": "Python Developer",
|
|
"user_resume": "John Doe\nSoftware Engineer\n\nExperience:\n- 5 years Python development\n- Django and Flask frameworks\n- PostgreSQL databases"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "Resume Corp" in data["content"]
|
|
# Prompt should reference the resume
|
|
assert "Resume/Background" in data["generation_prompt"]
|
|
|
|
def test_generate_cover_letter_unauthorized(self, test_client):
|
|
"""Test cover letter generation without authentication."""
|
|
request_data = {
|
|
"job_description": "Test job",
|
|
"company_name": "Test Corp",
|
|
"role_title": "Test Role"
|
|
}
|
|
|
|
response = test_client.post("/api/ai/generate-cover-letter", json=request_data)
|
|
|
|
assert response.status_code == 403 # HTTPBearer returns 403
|
|
|
|
def test_generate_cover_letter_invalid_token(self, test_client):
|
|
"""Test cover letter generation with invalid token."""
|
|
headers = {"Authorization": "Bearer invalid.token.here"}
|
|
request_data = {
|
|
"job_description": "Test job",
|
|
"company_name": "Test Corp",
|
|
"role_title": "Test Role"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_generate_cover_letter_missing_fields(self, test_client, test_user_token):
|
|
"""Test cover letter generation with missing required fields."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Test job",
|
|
# Missing company_name and role_title
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 422 # Validation error
|
|
|
|
def test_optimize_resume_success(self, test_client, test_user_token):
|
|
"""Test successful resume optimization."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"current_resume": """
|
|
John Smith
|
|
Software Engineer
|
|
|
|
Experience:
|
|
- 3 years Python development
|
|
- Built REST APIs using Flask
|
|
- Database management with MySQL
|
|
- Team collaboration and code reviews
|
|
""",
|
|
"job_description": "Senior Python Developer role requiring FastAPI, PostgreSQL, and AI/ML experience. Must have 5+ years of experience.",
|
|
"role_title": "Senior Python Developer"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/optimize-resume",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "content" in data
|
|
assert "model_used" in data
|
|
assert "generation_prompt" in data
|
|
|
|
# Should include original resume content
|
|
assert "John Smith" in data["content"]
|
|
assert "Senior Python Developer" in data["content"]
|
|
assert data["model_used"] == "template"
|
|
|
|
def test_optimize_resume_unauthorized(self, test_client):
|
|
"""Test resume optimization without authentication."""
|
|
request_data = {
|
|
"current_resume": "Test resume",
|
|
"job_description": "Test job",
|
|
"role_title": "Test role"
|
|
}
|
|
|
|
response = test_client.post("/api/ai/optimize-resume", json=request_data)
|
|
|
|
assert response.status_code == 403
|
|
|
|
def test_test_ai_connection_success(self, test_client, test_user_token):
|
|
"""Test AI connection test endpoint."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
|
|
response = test_client.post("/api/ai/test-ai-connection", headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "claude_available" in data
|
|
assert "openai_available" in data
|
|
assert "user" in data
|
|
assert "test_generation" in data
|
|
|
|
# Without API keys, should show unavailable but test should succeed
|
|
assert data["claude_available"] == False
|
|
assert data["openai_available"] == False
|
|
assert data["test_generation"] == "success"
|
|
assert data["model_used"] == "template"
|
|
assert "content_preview" in data
|
|
|
|
def test_test_ai_connection_unauthorized(self, test_client):
|
|
"""Test AI connection test without authentication."""
|
|
response = test_client.post("/api/ai/test-ai-connection")
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
class TestAIAPIErrorHandling:
|
|
"""Test error handling in AI API endpoints."""
|
|
|
|
@patch('src.backend.services.ai_service.ai_service.generate_cover_letter')
|
|
def test_cover_letter_generation_service_error(self, mock_generate, test_client, test_user_token):
|
|
"""Test cover letter generation when AI service fails."""
|
|
# Mock the service to raise an exception
|
|
mock_generate.side_effect = Exception("AI service unavailable")
|
|
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Test job",
|
|
"company_name": "Error Corp",
|
|
"role_title": "Test Role"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 500
|
|
data = response.json()
|
|
assert "Failed to generate cover letter" in data["detail"]
|
|
|
|
@patch('src.backend.services.ai_service.ai_service.generate_resume_optimization')
|
|
def test_resume_optimization_service_error(self, mock_optimize, test_client, test_user_token):
|
|
"""Test resume optimization when AI service fails."""
|
|
mock_optimize.side_effect = Exception("Service error")
|
|
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"current_resume": "Test resume",
|
|
"job_description": "Test job",
|
|
"role_title": "Test role"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/optimize-resume",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 500
|
|
data = response.json()
|
|
assert "Failed to optimize resume" in data["detail"]
|
|
|
|
def test_cover_letter_with_large_payload(self, test_client, test_user_token):
|
|
"""Test cover letter generation with very large job description."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
|
|
# Create a very large job description
|
|
large_description = "A" * 50000 # 50KB of text
|
|
|
|
request_data = {
|
|
"job_description": large_description,
|
|
"company_name": "Large Corp",
|
|
"role_title": "Big Role"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Should handle large payloads gracefully
|
|
assert response.status_code in [200, 413, 422] # Success or payload too large
|
|
|
|
def test_resume_optimization_empty_resume(self, test_client, test_user_token):
|
|
"""Test resume optimization with empty resume."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"current_resume": "",
|
|
"job_description": "Test job description",
|
|
"role_title": "Test Role"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/optimize-resume",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Should handle empty resume
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "content" in data
|
|
|
|
|
|
class TestAIAPIValidation:
|
|
"""Test input validation for AI API endpoints."""
|
|
|
|
def test_cover_letter_invalid_email_in_description(self, test_client, test_user_token):
|
|
"""Test cover letter generation with invalid characters."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Job with special chars: <script>alert('xss')</script>",
|
|
"company_name": "Security Corp",
|
|
"role_title": "Security Engineer"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Should sanitize or handle special characters
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# The script tag should not be executed (this is handled by the template)
|
|
assert "Security Corp" in data["content"]
|
|
|
|
def test_resume_optimization_unicode_content(self, test_client, test_user_token):
|
|
"""Test resume optimization with unicode characters."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"current_resume": "José González\nSoftware Engineer\n• 5 años de experiencia",
|
|
"job_description": "Seeking bilingual developer",
|
|
"role_title": "Desarrollador Senior"
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/optimize-resume",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "José González" in data["content"]
|
|
|
|
def test_cover_letter_null_values(self, test_client, test_user_token):
|
|
"""Test cover letter generation with null values in optional fields."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Test job description",
|
|
"company_name": "Null Corp",
|
|
"role_title": "Null Role",
|
|
"job_url": None,
|
|
"user_resume": None
|
|
}
|
|
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "Null Corp" in data["content"]
|
|
|
|
|
|
class TestAIAPIPerformance:
|
|
"""Test performance aspects of AI API endpoints."""
|
|
|
|
def test_concurrent_cover_letter_requests(self, test_client, test_user_token):
|
|
"""Test multiple concurrent cover letter requests."""
|
|
import threading
|
|
import time
|
|
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
|
|
def make_request(index):
|
|
request_data = {
|
|
"job_description": f"Job description {index}",
|
|
"company_name": f"Company {index}",
|
|
"role_title": f"Role {index}"
|
|
}
|
|
return test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Make 5 concurrent requests
|
|
start_time = time.time()
|
|
threads = []
|
|
results = []
|
|
|
|
for i in range(5):
|
|
thread = threading.Thread(target=lambda i=i: results.append(make_request(i)))
|
|
threads.append(thread)
|
|
thread.start()
|
|
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
end_time = time.time()
|
|
|
|
# All requests should succeed
|
|
assert len(results) == 5
|
|
for response in results:
|
|
assert response.status_code == 200
|
|
|
|
# Should complete in reasonable time (less than 10 seconds for template generation)
|
|
assert end_time - start_time < 10
|
|
|
|
def test_response_time_cover_letter(self, test_client, test_user_token):
|
|
"""Test response time for cover letter generation."""
|
|
import time
|
|
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Standard Python developer position",
|
|
"company_name": "Performance Corp",
|
|
"role_title": "Python Developer"
|
|
}
|
|
|
|
start_time = time.time()
|
|
response = test_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
end_time = time.time()
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Template generation should be fast (less than 1 second)
|
|
response_time = end_time - start_time
|
|
assert response_time < 1.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestAIAPIAsync:
|
|
"""Test AI API endpoints with async client."""
|
|
|
|
async def test_async_cover_letter_generation(self, async_client, test_user_token):
|
|
"""Test cover letter generation with async client."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"job_description": "Async job description",
|
|
"company_name": "Async Corp",
|
|
"role_title": "Async Developer"
|
|
}
|
|
|
|
response = await async_client.post(
|
|
"/api/ai/generate-cover-letter",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "Async Corp" in data["content"]
|
|
|
|
async def test_async_resume_optimization(self, async_client, test_user_token):
|
|
"""Test resume optimization with async client."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
request_data = {
|
|
"current_resume": "Async resume content",
|
|
"job_description": "Async job requirements",
|
|
"role_title": "Async Role"
|
|
}
|
|
|
|
response = await async_client.post(
|
|
"/api/ai/optimize-resume",
|
|
json=request_data,
|
|
headers=headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "Async resume content" in data["content"] |