from django.test import TestCase, Client from django.contrib.auth import get_user_model from django.urls import reverse from django.utils import timezone from django.core.files.uploadedfile import SimpleUploadedFile from datetime import datetime, time, timedelta import json from unittest.mock import patch, MagicMock User = get_user_model() from .models import ( JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview, TrainingMaterial, Source, HiringAgency, MeetingComment ) from .forms import ( JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, CandidateStageForm, BulkInterviewTemplateForm, CandidateSignupForm ) from .views import ( ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view, applications_exam_view, applications_interview_view, api_schedule_application_meeting ) from .views_frontend import CandidateListView, JobListView from .utils import create_zoom_meeting, get_applications_from_request class BaseTestCase(TestCase): """Base test case setup with common test data""" def setUp(self): self.client = Client() self.user = User.objects.create_user( username='testuser', email='test@example.com', password='testpass123', is_staff=True ) # Create test data self.job = JobPosting.objects.create( title='Software Engineer', department='IT', job_type='FULL_TIME', workplace_type='REMOTE', location_country='Saudi Arabia', description='Job description', qualifications='Job qualifications', application_deadline=timezone.now() + timedelta(days=30), created_by=self.user ) # Create a person first person = Person.objects.create( first_name='John', last_name='Doe', email='john@example.com', phone='1234567890' ) self.candidate = Application.objects.create( person=person, resume=SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf'), job=self.job, stage='Applied' ) self.zoom_meeting = ZoomMeeting.objects.create( topic='Interview with John Doe', start_time=timezone.now() + timedelta(hours=1), duration=60, timezone='UTC', join_url='https://zoom.us/j/123456789', meeting_id='123456789' ) class ModelTests(BaseTestCase): """Test cases for models""" def test_job_posting_creation(self): """Test JobPosting model creation""" self.assertEqual(self.job.title, 'Software Engineer') self.assertEqual(self.job.department, 'IT') self.assertIsNotNone(self.job.slug) self.assertEqual(self.job.status, 'DRAFT') def test_job_posting_unique_id_generation(self): """Test unique internal job ID generation""" self.assertTrue(self.job.internal_job_id.startswith('KAAUH')) self.assertIn(str(timezone.now().year), self.job.internal_job_id) def test_job_posting_methods(self): """Test JobPosting model methods""" # Test is_expired method self.assertFalse(self.job.is_expired()) # Test location display self.assertIn('Saudi Arabia', self.job.get_location_display()) def test_candidate_creation(self): """Test Candidate model creation""" self.assertEqual(self.candidate.first_name, 'John') self.assertEqual(self.candidate.stage, 'Applied') self.assertEqual(self.candidate.job, self.job) def test_candidate_stage_transitions(self): """Test candidate stage transition logic""" # Test current available stages available_stages = self.candidate.get_available_stages() self.assertIn('Exam', available_stages) self.assertIn('Interview', available_stages) def test_zoom_meeting_creation(self): """Test ZoomMeeting model creation""" self.assertEqual(self.zoom_meeting.topic, 'Interview with John Doe') self.assertEqual(self.zoom_meeting.duration, 60) self.assertIsNotNone(self.zoom_meeting.meeting_id) def test_template_creation(self): """Test FormTemplate model creation""" template = FormTemplate.objects.create( name='Test Template', job=self.job, created_by=self.user ) self.assertEqual(template.name, 'Test Template') self.assertEqual(template.job, self.job) def test_scheduled_interview_creation(self): """Test ScheduledInterview model creation""" scheduled = ScheduledInterview.objects.create( candidate=self.candidate, job=self.job, zoom_meeting=self.zoom_meeting, interview_date=timezone.now().date(), interview_time=time(10, 0), status='scheduled' ) self.assertEqual(scheduled.candidate, self.candidate) self.assertEqual(scheduled.status, 'scheduled') class ViewTests(BaseTestCase): """Test cases for views""" def test_job_list_view(self): """Test JobListView""" response = self.client.get(reverse('job_list')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Software Engineer') def test_job_list_search(self): """Test JobListView search functionality""" response = self.client.get(reverse('job_list'), {'search': 'Software'}) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Software Engineer') def test_job_detail_view(self): """Test job_detail view""" response = self.client.get(reverse('job_detail', kwargs={'slug': self.job.slug})) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Software Engineer') self.assertContains(response, 'John Doe') def test_zoom_meeting_list_view(self): """Test ZoomMeetingListView""" response = self.client.get(reverse('list_meetings')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Interview with John Doe') def test_zoom_meeting_list_search(self): """Test ZoomMeetingListView search functionality""" response = self.client.get(reverse('list_meetings'), {'q': 'Interview'}) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Interview with John Doe') def test_zoom_meeting_list_filter_status(self): """Test ZoomMeetingListView status filter""" response = self.client.get(reverse('list_meetings'), {'status': 'waiting'}) self.assertEqual(response.status_code, 200) def test_zoom_meeting_create_view(self): """Test ZoomMeetingCreateView""" self.client.login(username='testuser', password='testpass123') response = self.client.get(reverse('create_meeting')) self.assertEqual(response.status_code, 200) def test_candidate_screening_view(self): """Test candidate_screening_view""" response = self.client.get(reverse('applications_screening_view', kwargs={'slug': self.job.slug})) self.assertEqual(response.status_code, 200) self.assertContains(response, 'John Doe') def test_candidate_screening_view_filters(self): """Test candidate_screening_view with filters""" response = self.client.get( reverse('applications_screening_view', kwargs={'slug': self.job.slug}), {'min_ai_score': '50', 'tier1_count': '5'} ) self.assertEqual(response.status_code, 200) def test_candidate_exam_view(self): """Test candidate_exam_view""" response = self.client.get(reverse('applications_exam_view', kwargs={'slug': self.job.slug})) self.assertEqual(response.status_code, 200) self.assertContains(response, 'John Doe') def test_candidate_interview_view(self): """Test applications_interview_view""" response = self.client.get(reverse('applications_interview_view', kwargs={'slug': self.job.slug})) self.assertEqual(response.status_code, 200) @patch('recruitment.views.create_zoom_meeting') def test_schedule_candidate_meeting(self, mock_create_zoom): """Test api_schedule_application_meeting view""" mock_create_zoom.return_value = { 'status': 'success', 'meeting_details': { 'meeting_id': '987654321', 'join_url': 'https://zoom.us/j/987654321', 'password': '123456' }, 'zoom_gateway_response': {'status': 'waiting'} } self.client.login(username='testuser', password='testpass123') data = { 'start_time': (timezone.now() + timedelta(hours=2)).isoformat(), 'duration': 60 } response = self.client.post( reverse('api_schedule_application_meeting', kwargs={'job_slug': self.job.slug, 'candidate_pk': self.candidate.pk}), data ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'success') class FormTests(BaseTestCase): """Test cases for forms""" def test_job_posting_form(self): """Test JobPostingForm""" form_data = { 'title': 'New Job Title', 'department': 'New Department', 'job_type': 'FULL_TIME', 'workplace_type': 'REMOTE', 'location_city': 'Riyadh', 'location_state': 'Riyadh', 'location_country': 'Saudi Arabia', 'description': 'Job description with at least 20 characters to meet validation requirements', 'qualifications': 'Job qualifications', 'salary_range': '5000-7000', 'application_deadline': '2025-12-31', 'max_applications': '100', 'open_positions': '2', 'hash_tags': '#hiring,#jobopening' } form = JobPostingForm(data=form_data) self.assertTrue(form.is_valid()) def test_candidate_form(self): """Test CandidateForm""" form_data = { 'job': self.job.id, 'first_name': 'Jane', 'last_name': 'Smith', 'phone': '9876543210', 'email': 'jane@example.com', 'resume': SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf') } form = CandidateForm(data=form_data, files=form_data) self.assertTrue(form.is_valid()) def test_zoom_meeting_form(self): """Test ZoomMeetingForm""" form_data = { 'topic': 'Test Meeting', 'start_time': (timezone.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M'), 'duration': 60 } form = ZoomMeetingForm(data=form_data) self.assertTrue(form.is_valid()) def test_meeting_comment_form(self): """Test MeetingCommentForm""" form_data = { 'content': 'This is a test comment' } form = MeetingCommentForm(data=form_data) self.assertTrue(form.is_valid()) def test_candidate_stage_form(self): """Test CandidateStageForm with valid transition""" form_data = { 'stage': 'Exam' } form = CandidateStageForm(data=form_data, instance=self.candidate) self.assertTrue(form.is_valid()) def test_interview_schedule_form(self): """Test BulkInterviewTemplateForm""" # Update candidate to Interview stage first self.candidate.stage = 'Interview' self.candidate.save() form_data = { 'candidates': [self.candidate.id], 'start_date': (timezone.now() + timedelta(days=1)).date(), 'end_date': (timezone.now() + timedelta(days=7)).date(), 'working_days': [0, 1, 2, 3, 4], # Monday to Friday } form = BulkInterviewTemplateForm(slug=self.job.slug, data=form_data) self.assertTrue(form.is_valid()) def test_candidate_signup_form_valid(self): """Test CandidateSignupForm with valid data""" form_data = { 'first_name': 'John', 'last_name': 'Doe', 'email': 'john.doe@example.com', 'phone': '+1234567890', 'password': 'SecurePass123', 'confirm_password': 'SecurePass123' } form = CandidateSignupForm(data=form_data) self.assertTrue(form.is_valid()) def test_candidate_signup_form_password_mismatch(self): """Test CandidateSignupForm with password mismatch""" form_data = { 'first_name': 'John', 'last_name': 'Doe', 'email': 'john.doe@example.com', 'phone': '+1234567890', 'password': 'SecurePass123', 'confirm_password': 'DifferentPass123' } form = CandidateSignupForm(data=form_data) self.assertFalse(form.is_valid()) self.assertIn('Passwords do not match', str(form.errors)) class IntegrationTests(BaseTestCase): """Integration tests for multiple components""" def test_candidate_journey(self): """Test the complete candidate journey from application to interview""" # 1. Create candidate person = Person.objects.create( first_name='Jane', last_name='Smith', email='jane@example.com', phone='9876543210' ) candidate = Application.objects.create( person=person, resume=SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf'), job=self.job, stage='Applied' ) # 2. Move to Exam stage candidate.stage = 'Exam' candidate.save() # 3. Move to Interview stage candidate.stage = 'Interview' candidate.save() # 4. Create interview schedule scheduled_interview = ScheduledInterview.objects.create( candidate=candidate, job=self.job, zoom_meeting=self.zoom_meeting, interview_date=timezone.now().date(), interview_time=time(10, 0), status='scheduled' ) # 5. Verify all stages and relationships self.assertEqual(Application.objects.count(), 2) self.assertEqual(ScheduledInterview.objects.count(), 1) self.assertEqual(candidate.stage, 'Interview') self.assertEqual(scheduled_interview.candidate, candidate) def test_meeting_candidate_association(self): """Test the association between meetings and candidates""" # Create a scheduled interview scheduled_interview = ScheduledInterview.objects.create( candidate=self.candidate, job=self.job, zoom_meeting=self.zoom_meeting, interview_date=timezone.now().date(), interview_time=time(10, 0), status='scheduled' ) # Verify the relationship self.assertEqual(self.zoom_meeting.interview, scheduled_interview) self.assertEqual(self.candidate.get_meetings().count(), 1) def test_form_submission_candidate_creation(self): """Test creating a candidate through form submission""" # Create a form template template = FormTemplate.objects.create( job=self.job, name='Application Form', created_by=self.user, is_active=True ) # Create form stages and fields stage = FormStage.objects.create( template=template, name='Personal Information', order=0 ) FormField.objects.create( stage=stage, label='First Name', field_type='text', order=0 ) FormField.objects.create( stage=stage, label='Last Name', field_type='text', order=1 ) FormField.objects.create( stage=stage, label='Email', field_type='email', order=2 ) # Submit form data form_data = { 'field_1': 'New', 'field_2': 'Candidate', 'field_3': 'new@example.com' } response = self.client.post( reverse('application_submit', kwargs={'template_id': template.id}), form_data ) # Verify candidate was created self.assertEqual(Application.objects.filter(person__email='new@example.com').count(), 1) class PerformanceTests(BaseTestCase): """Basic performance tests""" def test_large_dataset_pagination(self): """Test pagination with large datasets""" # Create many candidates for i in range(100): person = Person.objects.create( first_name=f'Candidate{i}', last_name=f'Test{i}', email=f'candidate{i}@example.com', phone=f'123456789{i}' ) Application.objects.create( person=person, resume=SimpleUploadedFile(f'resume{i}.pdf', b'file_content', content_type='application/pdf'), job=self.job, stage='Applied' ) # Test pagination response = self.client.get(reverse('application_list')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Candidate') class AuthenticationTests(BaseTestCase): """Authentication and permission tests""" def test_unauthorized_access(self): """Test that unauthorized users cannot access protected views""" # Create a non-staff user regular_user = User.objects.create_user( username='regularuser', email='regular@example.com', password='testpass123' ) # Try to access a view that requires staff privileges self.client.login(username='regularuser', password='testpass123') response = self.client.get(reverse('job_list')) # Should still be accessible for now (can be adjusted based on actual requirements) self.assertEqual(response.status_code, 200) def test_login_required(self): """Test that login is required for certain operations""" # Test form submission without login template = FormTemplate.objects.create( job=self.job, name='Test Template', created_by=self.user, is_active=True ) response = self.client.post( reverse('application_submit', kwargs={'template_id': template.id}), {} ) # Should redirect to login page self.assertEqual(response.status_code, 302) class EdgeCaseTests(BaseTestCase): """Tests for edge cases and error handling""" def test_invalid_job_id(self): """Test handling of invalid job slug""" response = self.client.get(reverse('job_detail', kwargs={'slug': 'invalid-slug'})) self.assertEqual(response.status_code, 404) def test_meeting_past_time(self): """Test handling of meeting with past time""" # This would be tested in the view that validates meeting time pass def test_duplicate_submission(self): """Test handling of duplicate form submissions""" # Create form template template = FormTemplate.objects.create( job=self.job, name='Test Template', created_by=self.user, is_active=True ) # Submit form twice response1 = self.client.post( reverse('application_submit', kwargs={'template_id': template.id}), {'field_1': 'John', 'field_2': 'Doe'} ) # This should be handled by the view logic # Currently, it would create a duplicate candidate # We can add validation to prevent this if needed def test_invalid_stage_transition(self): """Test invalid candidate stage transitions""" # Try to transition from Interview back to Applied (should be invalid) self.candidate.stage = 'Interview' self.candidate.save() # The model should prevent this through validation # This would be tested in the model's clean method or view logic pass class UtilityFunctionTests(BaseTestCase): """Tests for utility functions""" @patch('recruitment.views.create_zoom_meeting') def test_create_zoom_meeting_utility(self, mock_create): """Test the create_zoom_meeting utility function""" mock_create.return_value = { 'status': 'success', 'meeting_details': { 'meeting_id': '123456789', 'join_url': 'https://zoom.us/j/123456789' } } result = create_zoom_meeting( topic='Test Meeting', start_time=timezone.now() + timedelta(hours=1), duration=60 ) self.assertEqual(result['status'], 'success') self.assertIn('meeting_id', result['meeting_details']) def get_applications_from_request(self): """Test the get_applications_from_request utility function""" # This would be tested with a request that has candidate_ids pass # Factory classes for test data (can be expanded with factory_boy) class TestFactories: """Factory methods for creating test data""" @staticmethod def create_job_posting(**kwargs): defaults = { 'title': 'Test Job', 'department': 'Test Department', 'job_type': 'FULL_TIME', 'workplace_type': 'ON_SITE', 'location_country': 'Saudi Arabia', 'description': 'Test description', 'created_by': User.objects.create_user('factoryuser', 'factory@example.com', 'password') } defaults.update(kwargs) return JobPosting.objects.create(**defaults) @staticmethod def create_candidate(**kwargs): job = TestFactories.create_job_posting() person = Person.objects.create( first_name='Test', last_name='Candidate', email='test@example.com', phone='1234567890' ) defaults = { 'person': person, 'job': job, 'stage': 'Applied', 'resume': SimpleUploadedFile('resume.pdf', b'file_content', content_type='application/pdf') } defaults.update(kwargs) return Application.objects.create(**defaults) @staticmethod def create_zoom_meeting(**kwargs): defaults = { 'topic': 'Test Meeting', 'start_time': timezone.now() + timedelta(hours=1), 'duration': 60, 'timezone': 'UTC', 'join_url': 'https://zoom.us/j/test123', 'meeting_id': 'test123' } defaults.update(kwargs) return ZoomMeeting.objects.create(**defaults) # Test runner configuration (can be added to settings) """ TEST_RUNNER = 'django.test.runner.DiscoverRunner' TEST_DISCOVER_TOPS = ['recruitment'] """