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']
"""