2025-11-27 16:25:34 +03:00

650 lines
23 KiB
Python

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