This comprehensive update transforms Job Forge from a generic MVP concept to a production-ready Python/FastAPI web application prototype with complete documentation, testing infrastructure, and deployment procedures. ## 🏗️ Architecture Changes - Updated all documentation to reflect Python/FastAPI + Dash + PostgreSQL stack - Transformed from MVP concept to deployable web application prototype - Added comprehensive multi-tenant architecture with Row Level Security (RLS) - Integrated Claude API and OpenAI API for AI-powered document generation ## 📚 Documentation Overhaul - **CLAUDE.md**: Complete rewrite as project orchestrator for 4 specialized agents - **README.md**: New centralized documentation hub with organized navigation - **API Specification**: Updated with comprehensive FastAPI endpoint documentation - **Database Design**: Enhanced schema with RLS policies and performance optimization - **Architecture Guide**: Transformed to web application focus with deployment strategy ## 🏗️ New Documentation Structure - **docs/development/**: Python/FastAPI coding standards and development guidelines - **docs/infrastructure/**: Docker setup and server deployment procedures - **docs/testing/**: Comprehensive QA procedures with pytest integration - **docs/ai/**: AI prompt templates and examples (preserved from original) ## 🎯 Team Structure Updates - **.claude/agents/**: 4 new Python/FastAPI specialized agents - simplified_technical_lead.md: Architecture and technical guidance - fullstack_developer.md: FastAPI backend + Dash frontend implementation - simplified_qa.md: pytest testing and quality assurance - simplified_devops.md: Docker deployment and server infrastructure ## 🧪 Testing Infrastructure - **pytest.ini**: Complete pytest configuration with coverage requirements - **tests/conftest.py**: Comprehensive test fixtures and database setup - **tests/unit/**: Example unit tests for auth and application services - **tests/integration/**: API integration test examples - Support for async testing, AI service mocking, and database testing ## 🧹 Cleanup - Removed 9 duplicate/outdated documentation files - Eliminated conflicting technology references (Node.js/TypeScript) - Consolidated overlapping content into comprehensive guides - Cleaned up project structure for professional development workflow ## 🚀 Production Ready Features - Docker containerization for development and production - Server deployment procedures for prototype hosting - Security best practices with JWT authentication and RLS - Performance optimization with database indexing and caching - Comprehensive testing strategy with quality gates This update establishes Job Forge as a professional Python/FastAPI web application prototype ready for development and deployment. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
458 lines
17 KiB
Python
458 lines
17 KiB
Python
# Unit tests for application service
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
from datetime import datetime
|
|
import uuid
|
|
|
|
from app.schemas.application import ApplicationCreate, ApplicationUpdate
|
|
from app.crud.application import (
|
|
create_application,
|
|
get_application_by_id,
|
|
get_user_applications,
|
|
update_application,
|
|
delete_application
|
|
)
|
|
from app.services.ai.claude_service import ClaudeService
|
|
from app.models.application import ApplicationStatus
|
|
|
|
|
|
class TestApplicationCRUD:
|
|
"""Test application CRUD operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_application_success(self, test_db, test_user):
|
|
"""Test successful application creation."""
|
|
|
|
app_data = ApplicationCreate(
|
|
company_name="Google",
|
|
role_title="Senior Python Developer",
|
|
job_description="Python developer role with ML focus",
|
|
status="draft"
|
|
)
|
|
|
|
application = await create_application(test_db, app_data, test_user.id)
|
|
|
|
assert application.company_name == "Google"
|
|
assert application.role_title == "Senior Python Developer"
|
|
assert application.status == ApplicationStatus.DRAFT
|
|
assert application.user_id == test_user.id
|
|
assert application.id is not None
|
|
assert application.created_at is not None
|
|
assert application.updated_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_application_with_ai_generation(self, test_db, test_user, mock_claude_service):
|
|
"""Test application creation with AI cover letter generation."""
|
|
|
|
app_data = ApplicationCreate(
|
|
company_name="Microsoft",
|
|
role_title="Software Engineer",
|
|
job_description="Full-stack developer position with React and Python",
|
|
status="draft"
|
|
)
|
|
|
|
with patch('app.services.ai.claude_service.ClaudeService', return_value=mock_claude_service):
|
|
application = await create_application(test_db, app_data, test_user.id)
|
|
|
|
assert application.company_name == "Microsoft"
|
|
assert application.cover_letter is not None
|
|
assert len(application.cover_letter) > 100
|
|
assert "Dear Hiring Manager" in application.cover_letter
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_application_by_id_success(self, test_db, test_application):
|
|
"""Test getting application by ID."""
|
|
|
|
retrieved_app = await get_application_by_id(
|
|
test_db, test_application.id, test_application.user_id
|
|
)
|
|
|
|
assert retrieved_app is not None
|
|
assert retrieved_app.id == test_application.id
|
|
assert retrieved_app.company_name == test_application.company_name
|
|
assert retrieved_app.user_id == test_application.user_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_application_by_id_wrong_user(self, test_db, test_application):
|
|
"""Test getting application by ID with wrong user (RLS test)."""
|
|
|
|
wrong_user_id = str(uuid.uuid4())
|
|
|
|
retrieved_app = await get_application_by_id(
|
|
test_db, test_application.id, wrong_user_id
|
|
)
|
|
|
|
# Should return None due to RLS policy
|
|
assert retrieved_app is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_application_nonexistent(self, test_db, test_user):
|
|
"""Test getting non-existent application."""
|
|
|
|
fake_id = str(uuid.uuid4())
|
|
|
|
retrieved_app = await get_application_by_id(
|
|
test_db, fake_id, test_user.id
|
|
)
|
|
|
|
assert retrieved_app is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_applications(self, test_db, test_user):
|
|
"""Test getting all applications for a user."""
|
|
|
|
# Create multiple applications
|
|
app_data_list = [
|
|
ApplicationCreate(
|
|
company_name=f"Company{i}",
|
|
role_title=f"Role{i}",
|
|
job_description=f"Description {i}",
|
|
status="draft"
|
|
)
|
|
for i in range(3)
|
|
]
|
|
|
|
created_apps = []
|
|
for app_data in app_data_list:
|
|
app = await create_application(test_db, app_data, test_user.id)
|
|
created_apps.append(app)
|
|
|
|
await test_db.commit()
|
|
|
|
# Get user applications
|
|
user_apps = await get_user_applications(test_db, test_user.id)
|
|
|
|
assert len(user_apps) >= 3 # At least the 3 we created
|
|
|
|
# Verify all returned apps belong to user
|
|
for app in user_apps:
|
|
assert app.user_id == test_user.id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_application_success(self, test_db, test_application):
|
|
"""Test successful application update."""
|
|
|
|
update_data = ApplicationUpdate(
|
|
company_name="Updated Company",
|
|
status="applied"
|
|
)
|
|
|
|
updated_app = await update_application(
|
|
test_db, test_application.id, update_data, test_application.user_id
|
|
)
|
|
|
|
assert updated_app.company_name == "Updated Company"
|
|
assert updated_app.status == ApplicationStatus.APPLIED
|
|
assert updated_app.updated_at > updated_app.created_at
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_application_wrong_user(self, test_db, test_application):
|
|
"""Test updating application with wrong user."""
|
|
|
|
wrong_user_id = str(uuid.uuid4())
|
|
update_data = ApplicationUpdate(company_name="Hacked Company")
|
|
|
|
updated_app = await update_application(
|
|
test_db, test_application.id, update_data, wrong_user_id
|
|
)
|
|
|
|
# Should return None due to RLS policy
|
|
assert updated_app is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_application_success(self, test_db, test_application):
|
|
"""Test successful application deletion."""
|
|
|
|
app_id = test_application.id
|
|
user_id = test_application.user_id
|
|
|
|
deleted = await delete_application(test_db, app_id, user_id)
|
|
|
|
assert deleted is True
|
|
|
|
# Verify application is deleted
|
|
retrieved_app = await get_application_by_id(test_db, app_id, user_id)
|
|
assert retrieved_app is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_application_wrong_user(self, test_db, test_application):
|
|
"""Test deleting application with wrong user."""
|
|
|
|
wrong_user_id = str(uuid.uuid4())
|
|
|
|
deleted = await delete_application(
|
|
test_db, test_application.id, wrong_user_id
|
|
)
|
|
|
|
# Should return False due to RLS policy
|
|
assert deleted is False
|
|
|
|
|
|
class TestApplicationStatusTransitions:
|
|
"""Test application status transitions."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_transition_draft_to_applied(self, test_db, test_application):
|
|
"""Test status transition from draft to applied."""
|
|
|
|
# Initial status should be draft
|
|
assert test_application.status == ApplicationStatus.DRAFT
|
|
|
|
update_data = ApplicationUpdate(status="applied")
|
|
updated_app = await update_application(
|
|
test_db, test_application.id, update_data, test_application.user_id
|
|
)
|
|
|
|
assert updated_app.status == ApplicationStatus.APPLIED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_transition_applied_to_interview(self, test_db):
|
|
"""Test status transition from applied to interview."""
|
|
|
|
# Create application in applied status
|
|
app_data = ApplicationCreate(
|
|
company_name="Interview Corp",
|
|
role_title="Developer",
|
|
job_description="Developer role",
|
|
status="applied"
|
|
)
|
|
|
|
from tests.conftest import TestDataFactory
|
|
user_data = TestDataFactory.user_data("interview@test.com")
|
|
|
|
# Create user and application
|
|
from app.crud.user import create_user
|
|
from app.schemas.user import UserCreate
|
|
|
|
user = await create_user(test_db, UserCreate(**user_data))
|
|
application = await create_application(test_db, app_data, user.id)
|
|
await test_db.commit()
|
|
|
|
# Update to interview status
|
|
update_data = ApplicationUpdate(status="interview")
|
|
updated_app = await update_application(
|
|
test_db, application.id, update_data, user.id
|
|
)
|
|
|
|
assert updated_app.status == ApplicationStatus.INTERVIEW
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_status_transition(self, test_db, test_application):
|
|
"""Test invalid status value."""
|
|
|
|
update_data = ApplicationUpdate(status="invalid_status")
|
|
|
|
with pytest.raises(ValueError):
|
|
await update_application(
|
|
test_db, test_application.id, update_data, test_application.user_id
|
|
)
|
|
|
|
|
|
class TestApplicationFiltering:
|
|
"""Test application filtering and searching."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_applications_by_status(self, test_db, test_user):
|
|
"""Test filtering applications by status."""
|
|
|
|
# Create applications with different statuses
|
|
statuses = ["draft", "applied", "interview", "rejected"]
|
|
applications = []
|
|
|
|
for status in statuses:
|
|
app_data = ApplicationCreate(
|
|
company_name=f"Company-{status}",
|
|
role_title="Developer",
|
|
job_description="Test role",
|
|
status=status
|
|
)
|
|
app = await create_application(test_db, app_data, test_user.id)
|
|
applications.append(app)
|
|
|
|
await test_db.commit()
|
|
|
|
# Test filtering (this would require implementing filter functionality)
|
|
all_apps = await get_user_applications(test_db, test_user.id)
|
|
|
|
# Verify we have applications with different statuses
|
|
app_statuses = {app.status for app in all_apps}
|
|
assert len(app_statuses) >= 3 # Should have multiple statuses
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_applications_by_company(self, test_db, test_user):
|
|
"""Test searching applications by company name."""
|
|
|
|
companies = ["Google", "Microsoft", "Apple", "Amazon"]
|
|
|
|
for company in companies:
|
|
app_data = ApplicationCreate(
|
|
company_name=company,
|
|
role_title="Developer",
|
|
job_description="Test role",
|
|
status="draft"
|
|
)
|
|
await create_application(test_db, app_data, test_user.id)
|
|
|
|
await test_db.commit()
|
|
|
|
# Get all applications
|
|
all_apps = await get_user_applications(test_db, test_user.id)
|
|
|
|
# Verify we can find specific companies
|
|
company_names = {app.company_name for app in all_apps}
|
|
assert "Google" in company_names
|
|
assert "Microsoft" in company_names
|
|
|
|
|
|
class TestApplicationValidation:
|
|
"""Test application data validation."""
|
|
|
|
def test_application_create_validation(self):
|
|
"""Test ApplicationCreate schema validation."""
|
|
|
|
# Valid data
|
|
valid_data = {
|
|
"company_name": "Valid Company",
|
|
"role_title": "Software Developer",
|
|
"job_description": "Great opportunity",
|
|
"status": "draft"
|
|
}
|
|
|
|
app_create = ApplicationCreate(**valid_data)
|
|
assert app_create.company_name == "Valid Company"
|
|
assert app_create.status == "draft"
|
|
|
|
def test_application_create_invalid_data(self):
|
|
"""Test ApplicationCreate with invalid data."""
|
|
|
|
# Missing required fields
|
|
with pytest.raises(ValueError):
|
|
ApplicationCreate(company_name="Company") # Missing role_title
|
|
|
|
# Invalid status
|
|
with pytest.raises(ValueError):
|
|
ApplicationCreate(
|
|
company_name="Company",
|
|
role_title="Role",
|
|
status="invalid_status"
|
|
)
|
|
|
|
def test_application_update_validation(self):
|
|
"""Test ApplicationUpdate schema validation."""
|
|
|
|
# Partial update should work
|
|
update_data = ApplicationUpdate(company_name="New Company")
|
|
assert update_data.company_name == "New Company"
|
|
|
|
# Update with valid status
|
|
update_data = ApplicationUpdate(status="applied")
|
|
assert update_data.status == "applied"
|
|
|
|
|
|
class TestConcurrentApplicationOperations:
|
|
"""Test concurrent operations on applications."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_application_updates(self, test_db, test_application):
|
|
"""Test concurrent updates to same application."""
|
|
|
|
import asyncio
|
|
|
|
async def update_company_name(name_suffix):
|
|
update_data = ApplicationUpdate(
|
|
company_name=f"Updated Company {name_suffix}"
|
|
)
|
|
return await update_application(
|
|
test_db, test_application.id, update_data, test_application.user_id
|
|
)
|
|
|
|
# Perform concurrent updates
|
|
tasks = [
|
|
update_company_name(i) for i in range(3)
|
|
]
|
|
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# At least one update should succeed
|
|
successful_updates = [r for r in results if not isinstance(r, Exception)]
|
|
assert len(successful_updates) >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_application_creation(self, test_db, test_user):
|
|
"""Test concurrent application creation for same user."""
|
|
|
|
import asyncio
|
|
|
|
async def create_test_application(index):
|
|
app_data = ApplicationCreate(
|
|
company_name=f"Concurrent Company {index}",
|
|
role_title=f"Role {index}",
|
|
job_description="Concurrent test",
|
|
status="draft"
|
|
)
|
|
return await create_application(test_db, app_data, test_user.id)
|
|
|
|
# Create multiple applications concurrently
|
|
tasks = [create_test_application(i) for i in range(5)]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# All creations should succeed
|
|
successful_creations = [r for r in results if not isinstance(r, Exception)]
|
|
assert len(successful_creations) == 5
|
|
|
|
# Verify all have different IDs
|
|
app_ids = {app.id for app in successful_creations}
|
|
assert len(app_ids) == 5
|
|
|
|
|
|
class TestApplicationBusinessLogic:
|
|
"""Test application business logic."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_application_timestamps_on_update(self, test_db, test_application):
|
|
"""Test that updated_at timestamp changes on update."""
|
|
|
|
original_updated_at = test_application.updated_at
|
|
|
|
# Wait a small amount to ensure timestamp difference
|
|
import asyncio
|
|
await asyncio.sleep(0.01)
|
|
|
|
update_data = ApplicationUpdate(company_name="Timestamp Test Company")
|
|
updated_app = await update_application(
|
|
test_db, test_application.id, update_data, test_application.user_id
|
|
)
|
|
|
|
assert updated_app.updated_at > original_updated_at
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_application_cover_letter_generation_trigger(self, test_db, test_user, mock_claude_service):
|
|
"""Test that cover letter generation is triggered appropriately."""
|
|
|
|
with patch('app.services.ai.claude_service.ClaudeService', return_value=mock_claude_service):
|
|
|
|
# Create application without job description
|
|
app_data = ApplicationCreate(
|
|
company_name="No Description Corp",
|
|
role_title="Developer",
|
|
status="draft"
|
|
)
|
|
|
|
app_without_desc = await create_application(test_db, app_data, test_user.id)
|
|
|
|
# Should not generate cover letter without job description
|
|
assert app_without_desc.cover_letter is None
|
|
|
|
# Create application with job description
|
|
app_data_with_desc = ApplicationCreate(
|
|
company_name="With Description Corp",
|
|
role_title="Developer",
|
|
job_description="Detailed job description here",
|
|
status="draft"
|
|
)
|
|
|
|
app_with_desc = await create_application(test_db, app_data_with_desc, test_user.id)
|
|
|
|
# Should generate cover letter with job description
|
|
assert app_with_desc.cover_letter is not None
|
|
assert len(app_with_desc.cover_letter) > 50 |