developed files

This commit is contained in:
2025-08-02 20:51:59 -04:00
parent c9f25ea149
commit 2d6c3bff56
24 changed files with 2660 additions and 220 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Tests package for Job Forge

View File

@@ -1,34 +1,32 @@
# Test configuration for Job Forge
"""
Updated test configuration for Job Forge that matches the actual project structure
"""
import pytest
import asyncio
import asyncpg
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from httpx import AsyncClient
import os
from typing import AsyncGenerator
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, Mock
import uuid
from app.main import app
from app.core.database import get_db, Base
from app.core.security import create_access_token
from app.models.user import User
from app.models.application import Application
# Fix import paths to match actual structure
from src.backend.main import app
from src.backend.core.database import get_db, Base
from src.backend.models.user import User
from src.backend.api.auth import create_access_token, hash_password
# Test database URL
# Test database URL (use a separate test database)
TEST_DATABASE_URL = os.getenv(
"TEST_DATABASE_URL",
"postgresql+asyncpg://jobforge:jobforge123@localhost:5432/jobforge_test"
"postgresql+asyncpg://jobforge_user:jobforge_password@localhost:5432/jobforge_test"
)
# Test engine and session factory
# Test engine
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
TestSessionLocal = sessionmaker(
test_engine, class_=AsyncSession, expire_on_commit=False
)
TestSessionLocal = sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
@pytest.fixture(scope="session")
def event_loop():
@@ -37,28 +35,13 @@ def event_loop():
yield loop
loop.close()
@pytest.fixture(scope="session")
async def setup_test_db():
"""Set up test database tables."""
# Create all tables
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
# Enable RLS and create policies
await conn.execute("""
ALTER TABLE applications ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS applications_user_isolation ON applications;
CREATE POLICY applications_user_isolation ON applications
FOR ALL TO authenticated
USING (user_id = current_setting('app.current_user_id')::UUID);
-- Create vector extension if needed
CREATE EXTENSION IF NOT EXISTS vector;
""")
yield
@@ -66,22 +49,18 @@ async def setup_test_db():
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
async def test_db(setup_test_db) -> AsyncGenerator[AsyncSession, None]:
"""Create a test database session."""
async with TestSessionLocal() as session:
try:
yield session
finally:
await session.rollback()
@pytest.fixture
def override_get_db(test_db: AsyncSession):
"""Override the get_db dependency for testing."""
def _override_get_db():
return test_db
@@ -89,147 +68,72 @@ def override_get_db(test_db: AsyncSession):
yield
app.dependency_overrides.clear()
@pytest.fixture
def test_client(override_get_db):
"""Create a test client."""
with TestClient(app) as client:
yield client
@pytest.fixture
async def async_client(override_get_db):
"""Create an async test client."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def test_user(test_db: AsyncSession):
"""Create a test user."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
user_data = UserCreate(
user = User(
email="test@jobforge.com",
password="testpassword123",
first_name="Test",
last_name="User"
password_hash=hash_password("testpassword123"),
full_name="Test User"
)
user = await create_user(test_db, user_data)
test_db.add(user)
await test_db.commit()
await test_db.refresh(user)
return user
@pytest.fixture
def test_user_token(test_user):
"""Create a JWT token for test user."""
token_data = {"sub": str(test_user.id), "email": test_user.email}
token_data = {"sub": str(test_user.id)}
return create_access_token(data=token_data)
@pytest.fixture
async def test_application(test_db: AsyncSession, test_user):
"""Create a test job application."""
from app.crud.application import create_application
from app.schemas.application import ApplicationCreate
app_data = ApplicationCreate(
company_name="Test Corp",
role_title="Software Developer",
job_description="Python developer position with FastAPI experience",
status="draft"
)
application = await create_application(test_db, app_data, test_user.id)
await test_db.commit()
return application
@pytest.fixture
def mock_claude_service():
"""Mock Claude AI service."""
mock = AsyncMock()
mock.generate_cover_letter.return_value = """
Dear Hiring Manager,
I am writing to express my strong interest in the Software Developer position at Test Corp.
With my experience in Python development and FastAPI expertise, I am confident I would be
a valuable addition to your team.
Thank you for your consideration.
Sincerely,
Test User
"""
def mock_ai_service():
"""Mock AI service for testing."""
mock = Mock()
mock.generate_cover_letter = AsyncMock(return_value={
"content": "Dear Hiring Manager,\n\nI am writing to express my interest...\n\nBest regards,\nTest User",
"model_used": "mock-ai",
"prompt": "Mock prompt for testing"
})
mock.generate_resume_optimization = AsyncMock(return_value={
"content": "Optimized Resume\n\nTest User\nSoftware Engineer\n\nExperience optimized for target role...",
"model_used": "mock-ai",
"prompt": "Mock resume optimization prompt"
})
return mock
@pytest.fixture
def mock_openai_service():
"""Mock OpenAI service."""
mock = AsyncMock()
mock.create_embedding.return_value = [0.1] * 1536 # Mock embedding vector
mock.test_connection.return_value = True
return mock
@pytest.fixture
async def multiple_test_users(test_db: AsyncSession):
"""Create multiple test users for isolation testing."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
"""Create multiple test users for testing."""
users = []
for i in range(3):
user_data = UserCreate(
user = User(
email=f"user{i}@test.com",
password="password123",
first_name=f"User{i}",
last_name="Test"
password_hash=hash_password("password123"),
full_name=f"User {i} Test"
)
user = await create_user(test_db, user_data)
test_db.add(user)
users.append(user)
await test_db.commit()
for user in users:
await test_db.refresh(user)
return users
@pytest.fixture
async def applications_for_users(test_db: AsyncSession, multiple_test_users):
"""Create applications for multiple users to test isolation."""
from app.crud.application import create_application
from app.schemas.application import ApplicationCreate
all_applications = []
for i, user in enumerate(multiple_test_users):
for j in range(2): # 2 applications per user
app_data = ApplicationCreate(
company_name=f"Company{i}-{j}",
role_title=f"Role{i}-{j}",
job_description=f"Job description for user {i}, application {j}",
status="draft"
)
application = await create_application(test_db, app_data, user.id)
all_applications.append(application)
await test_db.commit()
return all_applications
# Test data factories
class TestDataFactory:
"""Factory for creating test data."""
@@ -239,87 +143,46 @@ class TestDataFactory:
"""Create user test data."""
return {
"email": email or "test@example.com",
"password": "testpassword123",
"password": "testpassword123",
"first_name": "Test",
"last_name": "User",
**kwargs
}
@staticmethod
def application_data(company_name: str = None, **kwargs):
"""Create application test data."""
def cover_letter_request(**kwargs):
"""Create cover letter request data."""
return {
"company_name": company_name or "Test Company",
"role_title": "Software Developer",
"job_description": "Python developer position",
"status": "draft",
"job_description": "We are looking for a Software Engineer with Python experience",
"company_name": "Test Company",
"role_title": "Software Engineer",
**kwargs
}
@staticmethod
def ai_response():
"""Create mock AI response."""
return """
Dear Hiring Manager,
I am excited to apply for this position. My background in software development
and passion for technology make me an ideal candidate.
Best regards,
Test User
"""
def resume_optimization_request(**kwargs):
"""Create resume optimization request data."""
return {
"current_resume": "John Doe\nSoftware Engineer\n\nExperience:\n- Python development\n- Web applications",
"job_description": "Senior Python Developer role requiring FastAPI experience",
"role_title": "Senior Python Developer",
**kwargs
}
# Database utilities for testing
# Helper functions
async def create_test_user_and_token(db: AsyncSession, email: str):
"""Helper to create a user and return auth token."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
user_data = UserCreate(
user = User(
email=email,
password="password123",
first_name="Test",
last_name="User"
password_hash=hash_password("password123"),
full_name="Test User"
)
user = await create_user(db, user_data)
db.add(user)
await db.commit()
await db.refresh(user)
token_data = {"sub": str(user.id), "email": user.email}
token_data = {"sub": str(user.id)}
token = create_access_token(data=token_data)
return user, token
async def set_rls_context(db: AsyncSession, user_id: str):
"""Set RLS context for testing multi-tenancy."""
await db.execute(f"SET app.current_user_id = '{user_id}'")
# Performance testing helpers
@pytest.fixture
def benchmark_db_operations():
"""Benchmark database operations."""
import time
class BenchmarkContext:
def __init__(self):
self.start_time = None
self.end_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, *args):
self.end_time = time.time()
@property
def duration(self):
return self.end_time - self.start_time if self.end_time else None
return BenchmarkContext
return user, token

325
tests/conftest_old.py Normal file
View File

@@ -0,0 +1,325 @@
# Test configuration for Job Forge
import pytest
import asyncio
import asyncpg
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from httpx import AsyncClient
import os
from typing import AsyncGenerator
from unittest.mock import AsyncMock
from app.main import app
from app.core.database import get_db, Base
from app.core.security import create_access_token
from app.models.user import User
from app.models.application import Application
# Test database URL
TEST_DATABASE_URL = os.getenv(
"TEST_DATABASE_URL",
"postgresql+asyncpg://jobforge:jobforge123@localhost:5432/jobforge_test"
)
# Test engine and session factory
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
TestSessionLocal = sessionmaker(
test_engine, class_=AsyncSession, expire_on_commit=False
)
@pytest.fixture(scope="session")
def event_loop():
"""Create an instance of the default event loop for the test session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def setup_test_db():
"""Set up test database tables."""
# Create all tables
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
# Enable RLS and create policies
await conn.execute("""
ALTER TABLE applications ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS applications_user_isolation ON applications;
CREATE POLICY applications_user_isolation ON applications
FOR ALL TO authenticated
USING (user_id = current_setting('app.current_user_id')::UUID);
-- Create vector extension if needed
CREATE EXTENSION IF NOT EXISTS vector;
""")
yield
# Cleanup
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
async def test_db(setup_test_db) -> AsyncGenerator[AsyncSession, None]:
"""Create a test database session."""
async with TestSessionLocal() as session:
try:
yield session
finally:
await session.rollback()
@pytest.fixture
def override_get_db(test_db: AsyncSession):
"""Override the get_db dependency for testing."""
def _override_get_db():
return test_db
app.dependency_overrides[get_db] = _override_get_db
yield
app.dependency_overrides.clear()
@pytest.fixture
def test_client(override_get_db):
"""Create a test client."""
with TestClient(app) as client:
yield client
@pytest.fixture
async def async_client(override_get_db):
"""Create an async test client."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def test_user(test_db: AsyncSession):
"""Create a test user."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
user_data = UserCreate(
email="test@jobforge.com",
password="testpassword123",
first_name="Test",
last_name="User"
)
user = await create_user(test_db, user_data)
await test_db.commit()
return user
@pytest.fixture
def test_user_token(test_user):
"""Create a JWT token for test user."""
token_data = {"sub": str(test_user.id), "email": test_user.email}
return create_access_token(data=token_data)
@pytest.fixture
async def test_application(test_db: AsyncSession, test_user):
"""Create a test job application."""
from app.crud.application import create_application
from app.schemas.application import ApplicationCreate
app_data = ApplicationCreate(
company_name="Test Corp",
role_title="Software Developer",
job_description="Python developer position with FastAPI experience",
status="draft"
)
application = await create_application(test_db, app_data, test_user.id)
await test_db.commit()
return application
@pytest.fixture
def mock_claude_service():
"""Mock Claude AI service."""
mock = AsyncMock()
mock.generate_cover_letter.return_value = """
Dear Hiring Manager,
I am writing to express my strong interest in the Software Developer position at Test Corp.
With my experience in Python development and FastAPI expertise, I am confident I would be
a valuable addition to your team.
Thank you for your consideration.
Sincerely,
Test User
"""
return mock
@pytest.fixture
def mock_openai_service():
"""Mock OpenAI service."""
mock = AsyncMock()
mock.create_embedding.return_value = [0.1] * 1536 # Mock embedding vector
mock.test_connection.return_value = True
return mock
@pytest.fixture
async def multiple_test_users(test_db: AsyncSession):
"""Create multiple test users for isolation testing."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
users = []
for i in range(3):
user_data = UserCreate(
email=f"user{i}@test.com",
password="password123",
first_name=f"User{i}",
last_name="Test"
)
user = await create_user(test_db, user_data)
users.append(user)
await test_db.commit()
return users
@pytest.fixture
async def applications_for_users(test_db: AsyncSession, multiple_test_users):
"""Create applications for multiple users to test isolation."""
from app.crud.application import create_application
from app.schemas.application import ApplicationCreate
all_applications = []
for i, user in enumerate(multiple_test_users):
for j in range(2): # 2 applications per user
app_data = ApplicationCreate(
company_name=f"Company{i}-{j}",
role_title=f"Role{i}-{j}",
job_description=f"Job description for user {i}, application {j}",
status="draft"
)
application = await create_application(test_db, app_data, user.id)
all_applications.append(application)
await test_db.commit()
return all_applications
# Test data factories
class TestDataFactory:
"""Factory for creating test data."""
@staticmethod
def user_data(email: str = None, **kwargs):
"""Create user test data."""
return {
"email": email or "test@example.com",
"password": "testpassword123",
"first_name": "Test",
"last_name": "User",
**kwargs
}
@staticmethod
def application_data(company_name: str = None, **kwargs):
"""Create application test data."""
return {
"company_name": company_name or "Test Company",
"role_title": "Software Developer",
"job_description": "Python developer position",
"status": "draft",
**kwargs
}
@staticmethod
def ai_response():
"""Create mock AI response."""
return """
Dear Hiring Manager,
I am excited to apply for this position. My background in software development
and passion for technology make me an ideal candidate.
Best regards,
Test User
"""
# Database utilities for testing
async def create_test_user_and_token(db: AsyncSession, email: str):
"""Helper to create a user and return auth token."""
from app.crud.user import create_user
from app.schemas.user import UserCreate
user_data = UserCreate(
email=email,
password="password123",
first_name="Test",
last_name="User"
)
user = await create_user(db, user_data)
await db.commit()
token_data = {"sub": str(user.id), "email": user.email}
token = create_access_token(data=token_data)
return user, token
async def set_rls_context(db: AsyncSession, user_id: str):
"""Set RLS context for testing multi-tenancy."""
await db.execute(f"SET app.current_user_id = '{user_id}'")
# Performance testing helpers
@pytest.fixture
def benchmark_db_operations():
"""Benchmark database operations."""
import time
class BenchmarkContext:
def __init__(self):
self.start_time = None
self.end_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, *args):
self.end_time = time.time()
@property
def duration(self):
return self.end_time - self.start_time if self.end_time else None
return BenchmarkContext

View File

@@ -0,0 +1 @@
# Integration tests package

View File

@@ -0,0 +1,455 @@
"""
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"]

1
tests/unit/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Unit tests package

View File

@@ -0,0 +1,405 @@
"""
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

View File

@@ -0,0 +1,375 @@
"""
Unit tests for authentication endpoints
"""
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch
import uuid
from src.backend.main import app
from src.backend.models.user import User
from src.backend.api.auth import hash_password, verify_password, create_access_token
class TestAuthenticationAPI:
"""Test authentication API endpoints."""
def test_register_user_success(self, test_client, test_db):
"""Test successful user registration."""
user_data = {
"email": "newuser@test.com",
"password": "securepassword123",
"first_name": "New",
"last_name": "User"
}
response = test_client.post("/api/auth/register", json=user_data)
assert response.status_code == 200
data = response.json()
assert data["email"] == "newuser@test.com"
assert data["full_name"] == "New User"
assert data["first_name"] == "New"
assert data["last_name"] == "User"
assert data["is_active"] == True
assert "id" in data
# Password should never be returned
assert "password" not in data
assert "password_hash" not in data
def test_register_user_duplicate_email(self, test_client, test_user):
"""Test registration with duplicate email fails."""
user_data = {
"email": test_user.email, # Use existing user's email
"password": "differentpassword",
"first_name": "Duplicate",
"last_name": "User"
}
response = test_client.post("/api/auth/register", json=user_data)
assert response.status_code == 400
data = response.json()
assert "already registered" in data["detail"].lower()
def test_register_user_invalid_email(self, test_client):
"""Test registration with invalid email format."""
user_data = {
"email": "invalid-email-format",
"password": "securepassword123",
"first_name": "Invalid",
"last_name": "Email"
}
response = test_client.post("/api/auth/register", json=user_data)
assert response.status_code == 422 # Validation error
def test_register_user_missing_fields(self, test_client):
"""Test registration with missing required fields."""
user_data = {
"email": "incomplete@test.com",
# Missing password, first_name, last_name
}
response = test_client.post("/api/auth/register", json=user_data)
assert response.status_code == 422
def test_login_success(self, test_client, test_user):
"""Test successful login."""
login_data = {
"email": test_user.email,
"password": "testpassword123"
}
response = test_client.post("/api/auth/login", json=login_data)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "token_type" in data
assert data["token_type"] == "bearer"
# Verify token structure
token = data["access_token"]
assert len(token.split('.')) == 3 # JWT has 3 parts
def test_login_wrong_password(self, test_client, test_user):
"""Test login with incorrect password."""
login_data = {
"email": test_user.email,
"password": "wrongpassword"
}
response = test_client.post("/api/auth/login", json=login_data)
assert response.status_code == 401
data = response.json()
assert "incorrect" in data["detail"].lower()
def test_login_nonexistent_user(self, test_client):
"""Test login with non-existent user."""
login_data = {
"email": "nonexistent@test.com",
"password": "somepassword"
}
response = test_client.post("/api/auth/login", json=login_data)
assert response.status_code == 401
def test_login_invalid_email_format(self, test_client):
"""Test login with invalid email format."""
login_data = {
"email": "not-an-email",
"password": "somepassword"
}
response = test_client.post("/api/auth/login", json=login_data)
assert response.status_code == 422 # Validation error
def test_get_current_user_success(self, test_client, test_user_token):
"""Test getting current user with valid token."""
headers = {"Authorization": f"Bearer {test_user_token}"}
response = test_client.get("/api/auth/me", headers=headers)
assert response.status_code == 200
data = response.json()
assert "email" in data
assert "id" in data
assert "full_name" in data
assert "is_active" in data
# Ensure sensitive data is not returned
assert "password" not in data
assert "password_hash" not in data
def test_get_current_user_invalid_token(self, test_client):
"""Test getting current user with invalid token."""
headers = {"Authorization": "Bearer invalid.token.here"}
response = test_client.get("/api/auth/me", headers=headers)
assert response.status_code == 401
def test_get_current_user_no_token(self, test_client):
"""Test getting current user without authorization header."""
response = test_client.get("/api/auth/me")
assert response.status_code == 403 # FastAPI HTTPBearer returns 403
def test_get_current_user_malformed_header(self, test_client):
"""Test getting current user with malformed authorization header."""
malformed_headers = [
{"Authorization": "Bearer"},
{"Authorization": "NotBearer validtoken"},
{"Authorization": "Bearer "},
{"Authorization": "invalid-format"}
]
for headers in malformed_headers:
response = test_client.get("/api/auth/me", headers=headers)
assert response.status_code in [401, 403]
class TestPasswordUtilities:
"""Test password hashing and verification utilities."""
def test_hash_password(self):
"""Test password hashing function."""
password = "testpassword123"
hashed = hash_password(password)
assert hashed != password # Should be hashed
assert len(hashed) > 0
assert hashed.startswith('$2b$') # bcrypt format
def test_verify_password_correct(self):
"""Test password verification with correct password."""
password = "testpassword123"
hashed = hash_password(password)
assert verify_password(password, hashed) == True
def test_verify_password_incorrect(self):
"""Test password verification with incorrect password."""
password = "testpassword123"
wrong_password = "wrongpassword"
hashed = hash_password(password)
assert verify_password(wrong_password, hashed) == False
def test_hash_different_passwords_different_hashes(self):
"""Test that different passwords produce different hashes."""
password1 = "password123"
password2 = "password456"
hash1 = hash_password(password1)
hash2 = hash_password(password2)
assert hash1 != hash2
def test_hash_same_password_different_hashes(self):
"""Test that same password produces different hashes (salt)."""
password = "testpassword123"
hash1 = hash_password(password)
hash2 = hash_password(password)
assert hash1 != hash2 # Should be different due to salt
# But both should verify correctly
assert verify_password(password, hash1) == True
assert verify_password(password, hash2) == True
class TestJWTTokens:
"""Test JWT token creation and validation."""
def test_create_access_token(self):
"""Test JWT token creation."""
data = {"sub": str(uuid.uuid4()), "email": "test@example.com"}
token = create_access_token(data)
assert isinstance(token, str)
assert len(token.split('.')) == 3 # JWT format: header.payload.signature
def test_create_token_with_different_data(self):
"""Test that different data creates different tokens."""
data1 = {"sub": str(uuid.uuid4()), "email": "user1@example.com"}
data2 = {"sub": str(uuid.uuid4()), "email": "user2@example.com"}
token1 = create_access_token(data1)
token2 = create_access_token(data2)
assert token1 != token2
def test_token_contains_expiration(self):
"""Test that created tokens contain expiration claim."""
from jose import jwt
from src.backend.core.config import settings
data = {"sub": str(uuid.uuid4())}
token = create_access_token(data)
# Decode without verification to check claims
decoded = jwt.get_unverified_claims(token)
assert "exp" in decoded
assert "sub" in decoded
class TestUserModel:
"""Test User model properties and methods."""
def test_user_full_name_property(self):
"""Test that full_name property works correctly."""
user = User(
email="test@example.com",
password_hash="hashed_password",
full_name="John Doe"
)
assert user.full_name == "John Doe"
assert user.first_name == "John"
assert user.last_name == "Doe"
def test_user_single_name(self):
"""Test user with single name."""
user = User(
email="test@example.com",
password_hash="hashed_password",
full_name="Madonna"
)
assert user.full_name == "Madonna"
assert user.first_name == "Madonna"
assert user.last_name == ""
def test_user_multiple_last_names(self):
"""Test user with multiple last names."""
user = User(
email="test@example.com",
password_hash="hashed_password",
full_name="John van der Berg"
)
assert user.full_name == "John van der Berg"
assert user.first_name == "John"
assert user.last_name == "van der Berg"
def test_user_is_active_property(self):
"""Test user is_active property."""
user = User(
email="test@example.com",
password_hash="hashed_password",
full_name="Test User"
)
assert user.is_active == True # Default is True
class TestAuthenticationEdgeCases:
"""Test edge cases and error conditions."""
def test_register_empty_names(self, test_client):
"""Test registration with empty names."""
user_data = {
"email": "empty@test.com",
"password": "password123",
"first_name": "",
"last_name": ""
}
response = test_client.post("/api/auth/register", json=user_data)
# Should still work but create empty full_name
assert response.status_code == 200
data = response.json()
assert data["full_name"] == " " # Space between empty names
def test_register_very_long_email(self, test_client):
"""Test registration with very long email."""
long_email = "a" * 250 + "@test.com" # Very long email
user_data = {
"email": long_email,
"password": "password123",
"first_name": "Long",
"last_name": "Email"
}
response = test_client.post("/api/auth/register", json=user_data)
# Should handle long emails (within DB constraints)
if len(long_email) <= 255:
assert response.status_code == 200
else:
assert response.status_code in [400, 422]
def test_register_unicode_names(self, test_client):
"""Test registration with unicode characters in names."""
user_data = {
"email": "unicode@test.com",
"password": "password123",
"first_name": "José",
"last_name": "González"
}
response = test_client.post("/api/auth/register", json=user_data)
assert response.status_code == 200
data = response.json()
assert data["full_name"] == "José González"
assert data["first_name"] == "José"
assert data["last_name"] == "González"
def test_case_insensitive_email_login(self, test_client, test_user):
"""Test that email login is case insensitive."""
# Try login with different case
login_data = {
"email": test_user.email.upper(),
"password": "testpassword123"
}
response = test_client.post("/api/auth/login", json=login_data)
# This might fail if email comparison is case-sensitive
# The actual behavior depends on implementation
assert response.status_code in [200, 401]