627 lines
22 KiB
Python
627 lines
22 KiB
Python
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('application_submit', 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('application_submit', 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('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 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']
|
|
"""
|