developed files
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Tests package for Job Forge
|
||||
@@ -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
325
tests/conftest_old.py
Normal 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
|
||||
1
tests/integration/__init__.py
Normal file
1
tests/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Integration tests package
|
||||
455
tests/integration/test_ai_api_integration.py
Normal file
455
tests/integration/test_ai_api_integration.py
Normal 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
1
tests/unit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Unit tests package
|
||||
405
tests/unit/test_ai_service.py
Normal file
405
tests/unit/test_ai_service.py
Normal 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
|
||||
375
tests/unit/test_auth_endpoints.py
Normal file
375
tests/unit/test_auth_endpoints.py
Normal 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]
|
||||
Reference in New Issue
Block a user