# 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