1120 lines
40 KiB
Python
1120 lines
40 KiB
Python
"""
|
|
Advanced test cases for the recruitment application.
|
|
These tests cover complex scenarios, API integrations, and edge cases.
|
|
"""
|
|
|
|
from django.test import TestCase, Client, TransactionTestCase
|
|
from django.contrib.auth.models import User, Group
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import IntegrityError
|
|
from django.db.models import ProtectedError, Q
|
|
from django.test.utils import override_settings
|
|
from django.core.management import call_command
|
|
from django.conf import settings
|
|
from unittest.mock import patch, MagicMock, Mock
|
|
from datetime import datetime, time, timedelta, date
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from io import BytesIO
|
|
from PIL import Image
|
|
|
|
from .models import (
|
|
JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField,
|
|
FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview,
|
|
TrainingMaterial, Source, HiringAgency, MeetingComment, JobPostingImage,
|
|
BreakTime
|
|
)
|
|
from .forms import (
|
|
JobPostingForm, ApplicationForm, ZoomMeetingForm, MeetingCommentForm,
|
|
ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet
|
|
)
|
|
from .views import (
|
|
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
|
|
applications_exam_view, applications_interview_view, api_schedule_application_meeting,
|
|
schedule_interviews_view, confirm_schedule_interviews_view, _handle_preview_submission,
|
|
_handle_confirm_schedule, _handle_get_request
|
|
)
|
|
# from .views_frontend import CandidateListView, JobListView, JobCreateView
|
|
from .utils import (
|
|
create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,
|
|
get_zoom_meeting_details, get_applications_from_request,
|
|
get_available_time_slots
|
|
)
|
|
# from .zoom_api import ZoomAPIError
|
|
|
|
|
|
class AdvancedModelTests(TestCase):
|
|
"""Advanced model tests with complex scenarios"""
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='test@example.com',
|
|
password='testpass123',
|
|
is_staff=True
|
|
)
|
|
|
|
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,
|
|
max_applications=10,
|
|
open_positions=2
|
|
)
|
|
|
|
def test_job_posting_complex_validation(self):
|
|
"""Test complex validation scenarios for JobPosting"""
|
|
# Test with valid data
|
|
valid_data = {
|
|
'title': 'Valid Job Title',
|
|
'department': 'IT',
|
|
'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=valid_data)
|
|
self.assertTrue(form.is_valid())
|
|
|
|
def test_job_posting_invalid_data_scenarios(self):
|
|
"""Test various invalid data scenarios for JobPosting"""
|
|
# Test empty title
|
|
invalid_data = {
|
|
'title': '',
|
|
'department': 'IT',
|
|
'job_type': 'FULL_TIME',
|
|
'workplace_type': 'REMOTE'
|
|
}
|
|
form = JobPostingForm(data=invalid_data)
|
|
self.assertFalse(form.is_valid())
|
|
self.assertIn('title', form.errors)
|
|
|
|
# Test invalid max_applications
|
|
invalid_data['title'] = 'Test Job'
|
|
invalid_data['max_applications'] = '0'
|
|
form = JobPostingForm(data=invalid_data)
|
|
self.assertFalse(form.is_valid())
|
|
|
|
# Test invalid hash_tags
|
|
invalid_data['max_applications'] = '100'
|
|
invalid_data['hash_tags'] = 'invalid hash tags without #'
|
|
form = JobPostingForm(data=invalid_data)
|
|
self.assertFalse(form.is_valid())
|
|
|
|
def test_candidate_stage_transition_validation(self):
|
|
"""Test advanced candidate stage transition validation"""
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='John',
|
|
last_name='Doe',
|
|
email='john@example.com',
|
|
phone='1234567890'
|
|
),
|
|
job=self.job,
|
|
stage='Applied'
|
|
)
|
|
|
|
# Test valid transitions
|
|
valid_transitions = ['Exam', 'Interview', 'Offer']
|
|
for stage in valid_transitions:
|
|
application.stage = stage
|
|
application.save()
|
|
# Note: CandidateStageForm may need to be updated for Application model
|
|
# form = CandidateStageForm(data={'stage': stage}, candidate=application)
|
|
# self.assertTrue(form.is_valid())
|
|
|
|
# Test invalid transition (e.g., from Offer back to Applied)
|
|
application.stage = 'Offer'
|
|
application.save()
|
|
# Note: CandidateStageForm may need to be updated for Application model
|
|
# form = CandidateStageForm(data={'stage': 'Applied'}, candidate=application)
|
|
# This should fail based on your STAGE_SEQUENCE logic
|
|
# Note: You'll need to implement can_transition_to method in Application model
|
|
|
|
def test_zoom_meeting_conflict_detection(self):
|
|
"""Test conflict detection for overlapping meetings"""
|
|
# Create a meeting
|
|
meeting1 = ZoomMeeting.objects.create(
|
|
topic='Meeting 1',
|
|
start_time=timezone.now() + timedelta(hours=1),
|
|
duration=60,
|
|
timezone='UTC',
|
|
join_url='https://zoom.us/j/123456789',
|
|
meeting_id='123456789'
|
|
)
|
|
|
|
# Try to create overlapping meeting (this logic would be in your view/service)
|
|
# This is a placeholder for actual conflict detection implementation
|
|
with self.assertRaises(ValidationError):
|
|
# This would trigger your conflict validation
|
|
pass
|
|
|
|
def test_form_template_integrity(self):
|
|
"""Test form template data integrity"""
|
|
template = FormTemplate.objects.create(
|
|
job=self.job,
|
|
name='Test Template',
|
|
created_by=self.user
|
|
)
|
|
|
|
# Create stages
|
|
stage1 = FormStage.objects.create(template=template, name='Stage 1', order=0)
|
|
stage2 = FormStage.objects.create(template=template, name='Stage 2', order=1)
|
|
|
|
# Create fields
|
|
field1 = FormField.objects.create(
|
|
stage=stage1, label='Field 1', field_type='text', order=0
|
|
)
|
|
field2 = FormField.objects.create(
|
|
stage=stage1, label='Field 2', field_type='email', order=1
|
|
)
|
|
|
|
# Test stage ordering
|
|
stages = template.stages.all()
|
|
self.assertEqual(stages[0], stage1)
|
|
self.assertEqual(stages[1], stage2)
|
|
|
|
# Test field ordering within stage
|
|
fields = stage1.fields.all()
|
|
self.assertEqual(fields[0], field1)
|
|
self.assertEqual(fields[1], field2)
|
|
|
|
def test_interview_schedule_complex_validation(self):
|
|
"""Test interview schedule validation with complex constraints"""
|
|
# Create applications
|
|
application1 = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='John', last_name='Doe', email='john@example.com',
|
|
phone='1234567890'
|
|
),
|
|
job=self.job, stage='Interview'
|
|
)
|
|
application2 = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Jane', last_name='Smith', email='jane@example.com',
|
|
phone='9876543210'
|
|
),
|
|
job=self.job, stage='Interview'
|
|
)
|
|
|
|
# Create schedule with valid data
|
|
schedule_data = {
|
|
'candidates': [application1.id, application2.id],
|
|
'start_date': date.today() + timedelta(days=1),
|
|
'end_date': date.today() + timedelta(days=7),
|
|
'working_days': [0, 1, 2, 3, 4], # Mon-Fri
|
|
'start_time': '09:00',
|
|
'end_time': '17:00',
|
|
'interview_duration': 60,
|
|
'buffer_time': 15,
|
|
'break_start_time': '12:00',
|
|
'break_end_time': '13:00'
|
|
}
|
|
|
|
form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data)
|
|
self.assertTrue(form.is_valid())
|
|
|
|
def test_field_response_data_types(self):
|
|
"""Test different data types for field responses"""
|
|
# Create template and field
|
|
template = FormTemplate.objects.create(
|
|
job=self.job, name='Test Template', created_by=self.user
|
|
)
|
|
stage = FormStage.objects.create(template=template, name='Stage 1', order=0)
|
|
field = FormField.objects.create(
|
|
stage=stage, label='Test Field', field_type='text', order=0
|
|
)
|
|
|
|
# Create submission
|
|
submission = FormSubmission.objects.create(template=template)
|
|
|
|
# Test different value types
|
|
response = FieldResponse.objects.create(
|
|
submission=submission,
|
|
field=field,
|
|
value="Test string value"
|
|
)
|
|
self.assertEqual(response.display_value, "Test string value")
|
|
|
|
# Test list value (for checkbox/radio)
|
|
field.field_type = 'checkbox'
|
|
field.save()
|
|
response_checkbox = FieldResponse.objects.create(
|
|
submission=submission,
|
|
field=field,
|
|
value=["option1", "option2"]
|
|
)
|
|
self.assertEqual(response_checkbox.display_value, "option1, option2")
|
|
|
|
# Test file upload
|
|
file_content = b"Test file content"
|
|
uploaded_file = SimpleUploadedFile(
|
|
'test_file.pdf', file_content, content_type='application/pdf'
|
|
)
|
|
response_file = FieldResponse.objects.create(
|
|
submission=submission,
|
|
field=field,
|
|
uploaded_file=uploaded_file
|
|
)
|
|
self.assertTrue(response_file.is_file)
|
|
self.assertEqual(response_file.get_file_size, len(file_content))
|
|
|
|
|
|
class AdvancedViewTests(TestCase):
|
|
"""Advanced view tests with complex scenarios"""
|
|
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='test@example.com',
|
|
password='testpass123',
|
|
is_staff=True
|
|
)
|
|
|
|
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,
|
|
status='ACTIVE'
|
|
)
|
|
|
|
self.application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='John',
|
|
last_name='Doe',
|
|
email='john@example.com',
|
|
phone='1234567890'
|
|
),
|
|
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'
|
|
)
|
|
|
|
def test_job_detail_with_multiple_candidates(self):
|
|
"""Test job detail view with multiple candidates at different stages"""
|
|
# Create more applications at different stages
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Jane', last_name='Smith', email='jane@example.com',
|
|
phone='9876543210'
|
|
),
|
|
job=self.job, stage='Exam'
|
|
)
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Bob', last_name='Johnson', email='bob@example.com',
|
|
phone='5555555555'
|
|
),
|
|
job=self.job, stage='Interview'
|
|
)
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Alice', last_name='Brown', email='alice@example.com',
|
|
phone='4444444444'
|
|
),
|
|
job=self.job, stage='Offer'
|
|
)
|
|
|
|
response = self.client.get(reverse('job_detail', kwargs={'slug': self.job.slug}))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Check that counts are correct
|
|
self.assertContains(response, 'Total Applicants: 4')
|
|
self.assertContains(response, 'Applied: 1')
|
|
self.assertContains(response, 'Exam: 1')
|
|
self.assertContains(response, 'Interview: 1')
|
|
self.assertContains(response, 'Offer: 1')
|
|
|
|
def test_meeting_list_with_complex_filters(self):
|
|
"""Test meeting list view with multiple filter combinations"""
|
|
# Create meetings with different statuses and candidates
|
|
meeting2 = ZoomMeeting.objects.create(
|
|
topic='Interview with Jane Smith',
|
|
start_time=timezone.now() + timedelta(hours=2),
|
|
duration=60,
|
|
timezone='UTC',
|
|
join_url='https://zoom.us/j/987654321',
|
|
meeting_id='987654321',
|
|
status='started'
|
|
)
|
|
|
|
# Create scheduled interviews
|
|
ScheduledInterview.objects.create(
|
|
application=self.application,
|
|
job=self.job,
|
|
zoom_meeting=self.zoom_meeting,
|
|
interview_date=timezone.now().date(),
|
|
interview_time=time(10, 0),
|
|
status='scheduled'
|
|
)
|
|
|
|
ScheduledInterview.objects.create(
|
|
application=Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Jane', last_name='Smith', email='jane@example.com',
|
|
phone='9876543210'
|
|
),
|
|
job=self.job, stage='Interview'
|
|
),
|
|
job=self.job,
|
|
zoom_meeting=meeting2,
|
|
interview_date=timezone.now().date(),
|
|
interview_time=time(11, 0),
|
|
status='scheduled'
|
|
)
|
|
|
|
# Test combined filters
|
|
response = self.client.get(reverse('list_meetings'), {
|
|
'q': 'Interview',
|
|
'status': 'waiting',
|
|
'candidate_name': 'John'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_candidate_list_advanced_search(self):
|
|
"""Test candidate list view with advanced search functionality"""
|
|
# Create more applications for testing
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Jane', last_name='Smith', email='jane@example.com',
|
|
phone='9876543210'
|
|
),
|
|
job=self.job, stage='Exam'
|
|
)
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Bob', last_name='Johnson', email='bob@example.com',
|
|
phone='5555555555'
|
|
),
|
|
job=self.job, stage='Interview'
|
|
)
|
|
|
|
# Test search by name
|
|
response = self.client.get(reverse('application_list'), {
|
|
'search': 'Jane'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Jane Smith')
|
|
|
|
# Test search by email
|
|
response = self.client.get(reverse('application_list'), {
|
|
'search': 'bob@example.com'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'Bob Johnson')
|
|
|
|
# Test filter by job
|
|
response = self.client.get(reverse('application_list'), {
|
|
'job': self.job.slug
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Test filter by stage
|
|
response = self.client.get(reverse('application_list'), {
|
|
'stage': 'Exam'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_interview_scheduling_workflow(self):
|
|
"""Test the complete interview scheduling workflow"""
|
|
# Create applications for scheduling
|
|
applications = []
|
|
for i in range(3):
|
|
application = Application.objects.create(
|
|
person=Person.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='Interview'
|
|
)
|
|
applications.append(application)
|
|
|
|
# Test GET request (initial form)
|
|
request = self.client.get(reverse('schedule_interviews', kwargs={'slug': self.job.slug}))
|
|
self.assertEqual(request.status_code, 200)
|
|
|
|
# Test POST request with preview
|
|
with patch('recruitment.views.get_available_time_slots') as mock_slots:
|
|
# Mock available time slots
|
|
mock_slots.return_value = [
|
|
{'date': date.today() + timedelta(days=1), 'time': '10:00'},
|
|
{'date': date.today() + timedelta(days=1), 'time': '11:00'},
|
|
{'date': date.today() + timedelta(days=1), 'time': '14:00'}
|
|
]
|
|
|
|
# Test _handle_preview_submission
|
|
self.client.login(username='testuser', password='testpass123')
|
|
post_data = {
|
|
'candidates': [a.pk for a in applications],
|
|
'start_date': (date.today() + timedelta(days=1)).isoformat(),
|
|
'end_date': (date.today() + timedelta(days=7)).isoformat(),
|
|
'working_days': [0, 1, 2, 3, 4],
|
|
'start_time': '09:00',
|
|
'end_time': '17:00',
|
|
'interview_duration': '60',
|
|
'buffer_time': '15'
|
|
}
|
|
|
|
# This would normally be handled by the view, but we test the logic directly
|
|
# In a real test, you'd make a POST request to the view
|
|
request = self.client.post(
|
|
reverse('schedule_interviews', kwargs={'slug': self.job.slug}),
|
|
data=post_data
|
|
)
|
|
self.assertEqual(request.status_code, 200) # Should show preview
|
|
|
|
@patch('recruitment.views.create_zoom_meeting')
|
|
def test_meeting_creation_with_api_errors(self, mock_create):
|
|
"""Test meeting creation when API returns errors"""
|
|
# Test API error
|
|
mock_create.return_value = {
|
|
'status': 'error',
|
|
'message': 'Failed to create meeting'
|
|
}
|
|
|
|
self.client.login(username='testuser', password='testpass123')
|
|
data = {
|
|
'topic': 'Test Meeting',
|
|
'start_time': (timezone.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M'),
|
|
'duration': 60
|
|
}
|
|
|
|
response = self.client.post(reverse('create_meeting'), data)
|
|
# Should show error message
|
|
self.assertEqual(response.status_code, 200) # Form with error
|
|
|
|
def test_htmx_responses(self):
|
|
"""Test HTMX responses for partial updates"""
|
|
# Test HTMX request for candidate screening
|
|
response = self.client.get(
|
|
reverse('applications_screening_view', kwargs={'slug': self.job.slug}),
|
|
HTTP_HX_REQUEST='true'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Test HTMX request for meeting details
|
|
response = self.client.get(
|
|
reverse('meeting_details', kwargs={'slug': self.zoom_meeting.slug}),
|
|
HTTP_HX_REQUEST='true'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_bulk_operations(self):
|
|
"""Test bulk operations on candidates"""
|
|
# Create multiple applications
|
|
applications = []
|
|
for i in range(5):
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name=f'Bulk{i}',
|
|
last_name=f'Test{i}',
|
|
email=f'bulk{i}@example.com',
|
|
phone=f'123456789{i}'
|
|
),
|
|
job=self.job,
|
|
stage='Applied'
|
|
)
|
|
applications.append(application)
|
|
|
|
# Test bulk status update
|
|
application_ids = [a.pk for a in applications]
|
|
self.client.login(username='testuser', password='testpass123')
|
|
|
|
# This would be tested via a form submission
|
|
# For now, we test the view logic directly
|
|
request = self.client.post(
|
|
reverse('application_update_status', kwargs={'slug': self.job.slug}),
|
|
data={'candidate_ids': application_ids, 'mark_as': 'Exam'}
|
|
)
|
|
# Should redirect back to the view
|
|
self.assertEqual(request.status_code, 302)
|
|
|
|
# Verify applications were updated
|
|
updated_count = Application.objects.filter(
|
|
pk__in=application_ids,
|
|
stage='Exam'
|
|
).count()
|
|
self.assertEqual(updated_count, len(applications))
|
|
|
|
|
|
class AdvancedFormTests(TestCase):
|
|
"""Advanced form tests with complex validation scenarios"""
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='test@example.com',
|
|
password='testpass123',
|
|
is_staff=True
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
def test_complex_form_validation_scenarios(self):
|
|
"""Test complex validation scenarios for forms"""
|
|
# Test JobPostingForm with all field types
|
|
complex_data = {
|
|
'title': 'Senior Software Engineer',
|
|
'department': 'Engineering',
|
|
'job_type': 'FULL_TIME',
|
|
'workplace_type': 'HYBRID',
|
|
'location_city': 'Riyadh',
|
|
'location_state': 'Riyadh',
|
|
'location_country': 'Saudi Arabia',
|
|
'description': 'Detailed job description',
|
|
'qualifications': 'Detailed qualifications',
|
|
'salary_range': '8000-12000 SAR',
|
|
'benefits': 'Health insurance, annual leave',
|
|
'application_start_date': '2025-01-01',
|
|
'application_deadline': '2025-12-31',
|
|
'application_instructions': 'Submit your resume online',
|
|
'position_number': 'ENG-2025-001',
|
|
'reporting_to': 'Engineering Manager',
|
|
'joining_date': '2025-06-01',
|
|
'created_by': self.user.get_full_name(),
|
|
'open_positions': '3',
|
|
'hash_tags': '#tech, #engineering, #senior',
|
|
'max_applications': '200'
|
|
}
|
|
|
|
form = JobPostingForm(data=complex_data)
|
|
self.assertTrue(form.is_valid(), form.errors)
|
|
|
|
def test_form_dependency_validation(self):
|
|
"""Test validation for dependent form fields"""
|
|
# Test InterviewScheduleForm with dependent fields
|
|
schedule_data = {
|
|
'candidates': [], # Empty for now
|
|
'start_date': '2025-01-15',
|
|
'end_date': '2025-01-10', # Invalid: end_date before start_date
|
|
'working_days': [0, 1, 2, 3, 4],
|
|
'start_time': '09:00',
|
|
'end_time': '17:00',
|
|
'interview_duration': '60',
|
|
'buffer_time': '15'
|
|
}
|
|
|
|
form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data)
|
|
self.assertFalse(form.is_valid())
|
|
self.assertIn('end_date', form.errors)
|
|
|
|
def test_file_upload_validation(self):
|
|
"""Test file upload validation in forms"""
|
|
# Test valid file upload
|
|
valid_file = SimpleUploadedFile(
|
|
'valid_resume.pdf',
|
|
b'%PDF-1.4\n% ...',
|
|
content_type='application/pdf'
|
|
)
|
|
|
|
candidate_data = {
|
|
'job': self.job.id,
|
|
'first_name': 'John',
|
|
'last_name': 'Doe',
|
|
'phone': '1234567890',
|
|
'email': 'john@example.com',
|
|
'resume': valid_file
|
|
}
|
|
|
|
form = ApplicationForm(data=candidate_data, files=candidate_data)
|
|
self.assertTrue(form.is_valid())
|
|
|
|
# Test invalid file type (would need custom validator)
|
|
# This test depends on your actual file validation logic
|
|
|
|
def test_dynamic_form_fields(self):
|
|
"""Test forms with dynamically populated fields"""
|
|
# Test InterviewScheduleForm with dynamic candidate queryset
|
|
# Create applications in Interview stage
|
|
applications = []
|
|
for i in range(3):
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name=f'Interview{i}',
|
|
last_name=f'Candidate{i}',
|
|
email=f'interview{i}@example.com',
|
|
phone=f'123456789{i}'
|
|
),
|
|
job=self.job,
|
|
stage='Interview'
|
|
)
|
|
applications.append(application)
|
|
|
|
# Form should only show Interview stage applications
|
|
form = InterviewScheduleForm(slug=self.job.slug)
|
|
self.assertEqual(form.fields['candidates'].queryset.count(), 3)
|
|
|
|
for application in applications:
|
|
self.assertIn(application, form.fields['candidates'].queryset)
|
|
|
|
|
|
class AdvancedIntegrationTests(TransactionTestCase):
|
|
"""Advanced integration tests covering multiple components"""
|
|
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='test@example.com',
|
|
password='testpass123',
|
|
is_staff=True
|
|
)
|
|
|
|
def test_complete_hiring_workflow(self):
|
|
"""Test the complete hiring workflow from job posting to hire"""
|
|
# 1. Create job
|
|
job = JobPosting.objects.create(
|
|
title='Product Manager',
|
|
department='Product',
|
|
job_type='FULL_TIME',
|
|
workplace_type='ON_SITE',
|
|
location_country='Saudi Arabia',
|
|
description='Product Manager job description',
|
|
qualifications='Product management experience',
|
|
created_by=self.user,
|
|
status='ACTIVE'
|
|
)
|
|
|
|
# 2. Create form template for applications
|
|
template = FormTemplate.objects.create(
|
|
job=job,
|
|
name='Product Manager Application',
|
|
created_by=self.user,
|
|
is_active=True
|
|
)
|
|
|
|
# 3. Create form stages and fields
|
|
personal_stage = FormStage.objects.create(
|
|
template=template,
|
|
name='Personal Information',
|
|
order=0
|
|
)
|
|
|
|
FormField.objects.create(
|
|
stage=personal_stage,
|
|
label='First Name',
|
|
field_type='text',
|
|
order=0,
|
|
required=True
|
|
)
|
|
FormField.objects.create(
|
|
stage=personal_stage,
|
|
label='Last Name',
|
|
field_type='text',
|
|
order=1,
|
|
required=True
|
|
)
|
|
FormField.objects.create(
|
|
stage=personal_stage,
|
|
label='Email',
|
|
field_type='email',
|
|
order=2,
|
|
required=True
|
|
)
|
|
|
|
experience_stage = FormStage.objects.create(
|
|
template=template,
|
|
name='Work Experience',
|
|
order=1
|
|
)
|
|
|
|
FormField.objects.create(
|
|
stage=experience_stage,
|
|
label='Years of Experience',
|
|
field_type='number',
|
|
order=0
|
|
)
|
|
|
|
# 4. Submit application
|
|
submission_data = {
|
|
'field_1': 'Sarah',
|
|
'field_2': 'Johnson',
|
|
'field_3': 'sarah@example.com',
|
|
'field_4': '5'
|
|
}
|
|
|
|
response = self.client.post(
|
|
reverse('application_submit', kwargs={'template_id': template.id}),
|
|
submission_data
|
|
)
|
|
self.assertEqual(response.status_code, 302) # Redirect to success page
|
|
|
|
# 5. Verify application was created
|
|
application = Application.objects.get(person__email='sarah@example.com')
|
|
self.assertEqual(application.stage, 'Applied')
|
|
self.assertEqual(application.job, job)
|
|
|
|
# 6. Move application to Exam stage
|
|
application.stage = 'Exam'
|
|
application.save()
|
|
|
|
# 7. Move application to Interview stage
|
|
application.stage = 'Interview'
|
|
application.save()
|
|
|
|
# 8. Create interview schedule
|
|
scheduled_interview = ScheduledInterview.objects.create(
|
|
application=application,
|
|
job=job,
|
|
interview_date=timezone.now().date() + timedelta(days=7),
|
|
interview_time=time(14, 0),
|
|
status='scheduled'
|
|
)
|
|
|
|
# 9. Create Zoom meeting
|
|
zoom_meeting = ZoomMeeting.objects.create(
|
|
topic=f'Interview: {job.title} with {application.person.get_full_name()}',
|
|
start_time=timezone.now() + timedelta(days=7, hours=14),
|
|
duration=60,
|
|
timezone='UTC',
|
|
join_url='https://zoom.us/j/interview123',
|
|
meeting_id='interview123'
|
|
)
|
|
|
|
# 10. Assign meeting to interview
|
|
scheduled_interview.zoom_meeting = zoom_meeting
|
|
scheduled_interview.save()
|
|
|
|
# 11. Verify all relationships
|
|
self.assertEqual(application.scheduled_interviews.count(), 1)
|
|
self.assertEqual(zoom_meeting.interview, scheduled_interview)
|
|
self.assertEqual(job.applications.count(), 1)
|
|
|
|
# 12. Complete hire process
|
|
application.stage = 'Offer'
|
|
application.save()
|
|
|
|
# 13. Verify final state
|
|
self.assertEqual(Application.objects.filter(stage='Offer').count(), 1)
|
|
|
|
def test_data_integrity_across_operations(self):
|
|
"""Test data integrity across multiple operations"""
|
|
# Create complex data structure
|
|
job = JobPosting.objects.create(
|
|
title='Data Scientist',
|
|
department='Analytics',
|
|
job_type='FULL_TIME',
|
|
workplace_type='REMOTE',
|
|
location_country='Saudi Arabia',
|
|
description='Data Scientist position',
|
|
created_by=self.user,
|
|
max_applications=5
|
|
)
|
|
|
|
# Create multiple applications
|
|
applications = []
|
|
for i in range(3):
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name=f'Data{i}',
|
|
last_name=f'Scientist{i}',
|
|
email=f'data{i}@example.com',
|
|
phone=f'123456789{i}'
|
|
),
|
|
job=job,
|
|
stage='Applied'
|
|
)
|
|
applications.append(application)
|
|
|
|
# Create form template
|
|
template = FormTemplate.objects.create(
|
|
job=job,
|
|
name='Data Scientist Application',
|
|
created_by=self.user,
|
|
is_active=True
|
|
)
|
|
|
|
# Create submissions for applications
|
|
for i, application in enumerate(applications):
|
|
submission = FormSubmission.objects.create(
|
|
template=template,
|
|
applicant_name=f'{application.person.first_name} {application.person.last_name}',
|
|
applicant_email=application.person.email
|
|
)
|
|
|
|
# Create field responses
|
|
FieldResponse.objects.create(
|
|
submission=submission,
|
|
field=FormField.objects.create(
|
|
stage=FormStage.objects.create(template=template, name='Stage 1', order=0),
|
|
label='Test Field',
|
|
field_type='text'
|
|
),
|
|
value=f'Test response {i}'
|
|
)
|
|
|
|
# Verify data consistency
|
|
self.assertEqual(FormSubmission.objects.filter(template=template).count(), 3)
|
|
self.assertEqual(FieldResponse.objects.count(), 3)
|
|
|
|
# Test application limit
|
|
for i in range(3): # Try to add more applications than limit
|
|
Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name=f'Extra{i}',
|
|
last_name=f'Candidate{i}',
|
|
email=f'extra{i}@example.com',
|
|
phone=f'11111111{i}'
|
|
),
|
|
job=job,
|
|
stage='Applied'
|
|
)
|
|
|
|
# Verify that the job shows application limit warning
|
|
job.refresh_from_db()
|
|
self.assertTrue(job.is_application_limit_reached)
|
|
|
|
@patch('recruitment.views.create_zoom_meeting')
|
|
def test_zoom_integration_workflow(self, mock_create):
|
|
"""Test complete Zoom integration workflow"""
|
|
# Setup job and application
|
|
job = JobPosting.objects.create(
|
|
title='Remote Developer',
|
|
department='Engineering',
|
|
job_type='REMOTE',
|
|
created_by=self.user
|
|
)
|
|
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name='Remote',
|
|
last_name='Developer',
|
|
email='remote@example.com'
|
|
),
|
|
job=job,
|
|
stage='Interview'
|
|
)
|
|
|
|
# Mock successful Zoom meeting creation
|
|
mock_create.return_value = {
|
|
'status': 'success',
|
|
'meeting_details': {
|
|
'meeting_id': 'zoom123',
|
|
'join_url': 'https://zoom.us/j/zoom123',
|
|
'password': 'meeting123'
|
|
},
|
|
'zoom_gateway_response': {
|
|
'status': 'waiting',
|
|
'id': 'meeting_zoom123'
|
|
}
|
|
}
|
|
|
|
# Schedule meeting via API
|
|
with patch('recruitment.views.ScheduledInterview.objects.create') as mock_create_interview:
|
|
mock_create_interview.return_value = ScheduledInterview(
|
|
application=application,
|
|
job=job,
|
|
zoom_meeting=None,
|
|
interview_date=timezone.now().date(),
|
|
interview_time=time(15, 0),
|
|
status='scheduled'
|
|
)
|
|
|
|
response = self.client.post(
|
|
reverse('api_schedule_application_meeting',
|
|
kwargs={'job_slug': job.slug, 'candidate_pk': application.pk}),
|
|
data={
|
|
'start_time': (timezone.now() + timedelta(hours=1)).isoformat(),
|
|
'duration': 60
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'success')
|
|
|
|
# Verify Zoom API was called
|
|
mock_create.assert_called_once()
|
|
|
|
# Verify interview was created
|
|
mock_create_interview.assert_called_once()
|
|
|
|
def test_concurrent_operations(self):
|
|
"""Test handling of concurrent operations"""
|
|
# Create job
|
|
job = JobPosting.objects.create(
|
|
title='Concurrency Test',
|
|
department='Test',
|
|
created_by=self.user
|
|
)
|
|
|
|
# Create applications
|
|
applications = []
|
|
for i in range(10):
|
|
application = Application.objects.create(
|
|
person=Person.objects.create(
|
|
first_name=f'Concurrent{i}',
|
|
last_name=f'Test{i}',
|
|
email=f'concurrent{i}@example.com'
|
|
),
|
|
job=job,
|
|
stage='Applied'
|
|
)
|
|
applications.append(application)
|
|
|
|
# Test concurrent application updates
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
def update_application(application_id, stage):
|
|
from django.test import TestCase
|
|
from django.db import transaction
|
|
from recruitment.models import Application
|
|
|
|
with transaction.atomic():
|
|
application = Application.objects.select_for_update().get(pk=application_id)
|
|
application.stage = stage
|
|
application.save()
|
|
|
|
# Update applications concurrently
|
|
with ThreadPoolExecutor(max_workers=3) as executor:
|
|
futures = [
|
|
executor.submit(update_application, a.pk, 'Exam')
|
|
for a in applications
|
|
]
|
|
|
|
for future in futures:
|
|
future.result()
|
|
|
|
# Verify all updates completed
|
|
self.assertEqual(Application.objects.filter(stage='Exam').count(), len(applications))
|
|
|
|
|
|
class SecurityTests(TestCase):
|
|
"""Security-focused tests"""
|
|
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
email='test@example.com',
|
|
password='testpass123',
|
|
is_staff=False
|
|
)
|
|
self.staff_user = User.objects.create_user(
|
|
username='staffuser',
|
|
email='staff@example.com',
|
|
password='testpass123',
|
|
is_staff=True
|
|
)
|
|
|
|
self.job = JobPosting.objects.create(
|
|
title='Security Test Job',
|
|
department='Security',
|
|
job_type='FULL_TIME',
|
|
created_by=self.staff_user
|
|
)
|
|
|
|
def test_unauthorized_access_control(self):
|
|
"""Test that unauthorized users cannot access protected resources"""
|
|
# Test regular user accessing staff-only functionality
|
|
self.client.login(username='testuser', password='testpass123')
|
|
|
|
# Access job list (should be accessible)
|
|
response = self.client.get(reverse('job_list'))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Try to edit job (should be restricted based on your actual implementation)
|
|
response = self.client.get(reverse('job_update', kwargs={'slug': self.job.slug}))
|
|
# This depends on your actual access control implementation
|
|
# For now, we'll assume it redirects or shows 403
|
|
|
|
def test_csrf_protection(self):
|
|
"""Test CSRF protection on forms"""
|
|
# Test POST request without CSRF token (should fail)
|
|
self.client.login(username='staffuser', password='testpass123')
|
|
|
|
response = self.client.post(
|
|
reverse('create_meeting'),
|
|
data={
|
|
'topic': 'Test Meeting',
|
|
'start_time': (timezone.now() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M'),
|
|
'duration': 60
|
|
},
|
|
HTTP_X_CSRFTOKEN='invalid' # Invalid or missing CSRF token
|
|
)
|
|
# Should be blocked by Django's CSRF protection
|
|
# The exact behavior depends on your middleware setup
|
|
|
|
def test_sql_injection_prevention(self):
|
|
"""Test that forms prevent SQL injection"""
|
|
# Test SQL injection in form fields
|
|
malicious_input = "Robert'); DROP TABLE candidates;--"
|
|
|
|
form_data = {
|
|
'title': f'SQL Injection Test {malicious_input}',
|
|
'department': 'IT',
|
|
'job_type': 'FULL_TIME',
|
|
'workplace_type': 'REMOTE'
|
|
}
|
|
|
|
form = JobPostingForm(data=form_data)
|
|
# Form should still be valid (malicious input stored as text, not executed)
|
|
self.assertTrue(form.is_valid())
|
|
|
|
# The actual protection comes from Django's ORM parameterized queries
|
|
|
|
def test_xss_prevention(self):
|
|
"""Test that forms prevent XSS attacks"""
|
|
# Test XSS attempt in form fields
|
|
xss_script = '<script>alert("XSS")</script>'
|
|
|
|
form_data = {
|
|
'title': f'XSS Test {xss_script}',
|
|
'department': 'IT',
|
|
'job_type': 'FULL_TIME',
|
|
'workplace_type': 'REMOTE'
|
|
}
|
|
|
|
form = JobPostingForm(data=form_data)
|
|
self.assertTrue(form.is_valid())
|
|
|
|
# The actual protection should be in template rendering
|
|
# Test template rendering with potentially malicious content
|
|
job = JobPosting.objects.create(
|
|
title=f'XSS Test {xss_script}',
|
|
department='IT',
|
|
created_by=self.staff_user
|
|
)
|