from django.test import TestCase, Client from django.contrib.auth.models import User 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 from .models import ( JobPosting, Candidate, ZoomMeeting, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, TrainingMaterial, Source, HiringAgency, Profile, MeetingComment ) from .forms import ( JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, CandidateStageForm, InterviewScheduleForm ) from .views import ( ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view, candidate_exam_view, candidate_interview_view, submit_form, api_schedule_candidate_meeting ) from .views_frontend import CandidateListView, JobListView from .utils import create_zoom_meeting, get_candidates_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 ) self.profile = Profile.objects.create(user=self.user) # 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', created_by=self.user ) self.candidate = Candidate.objects.create( first_name='John', last_name='Doe', email='john@example.com', phone='1234567890', 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('candidate_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('candidate_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('candidate_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 candidate_interview_view""" response = self.client.get(reverse('candidate_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_candidate_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_candidate_meeting', kwargs={'job_slug': self.job.slug, 'candidate_pk': self.candidate.pk}), data ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'success') def test_submit_form(self): """Test submit_form view""" # Create a form template first template = FormTemplate.objects.create( job=self.job, name='Test Template', created_by=self.user, is_active=True ) data = { 'field_1': 'John', # Assuming field ID 1 corresponds to First Name 'field_2': 'Doe', # Assuming field ID 2 corresponds to Last Name 'field_3': 'john@example.com', # Email } response = self.client.post( reverse('submit_form', kwargs={'template_id': template.id}), data ) # After successful submission, should redirect to success page self.assertEqual(response.status_code, 302) 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', '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, candidate=self.candidate) self.assertTrue(form.is_valid()) def test_interview_schedule_form(self): """Test InterviewScheduleForm""" 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 'start_time': '09:00', 'end_time': '17:00', 'interview_duration': 60, 'buffer_time': 15 } form = InterviewScheduleForm(slug=self.job.slug, data=form_data) self.assertTrue(form.is_valid()) 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 candidate = Candidate.objects.create( first_name='Jane', last_name='Smith', email='jane@example.com', phone='9876543210', 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(Candidate.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('submit_form', kwargs={'template_id': template.id}), form_data ) # Verify candidate was created self.assertEqual(Candidate.objects.filter(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): Candidate.objects.create( first_name=f'Candidate{i}', last_name=f'Test{i}', email=f'candidate{i}@example.com', phone=f'123456789{i}', job=self.job, stage='Applied' ) # Test pagination response = self.client.get(reverse('candidate_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('submit_form', 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('submit_form', 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 test_get_candidates_from_request(self): """Test the get_candidates_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() defaults = { 'first_name': 'Test', 'last_name': 'Candidate', 'email': 'test@example.com', 'phone': '1234567890', 'job': job, 'stage': 'Applied' } defaults.update(kwargs) return Candidate.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'] """