Files
ACORE/apps/hiring/tests.py
T

435 lines
18 KiB
Python

"""Тести найму: пул інтернів, IT Ticket, Employee лише після approve, PDF."""
from datetime import date, timedelta
from unittest import mock
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import Client, TestCase
from django.urls import reverse
from core.models import Directory, DirectoryType
from apps.hiring.models import HiringRequest, InternAccount
from apps.hiring.services import approve_hiring_request, create_hiring_request, reject_hiring_request
from apps.it_manager.models import Ticket
from apps.locations.models import Location, Pharmacy
Employee = get_user_model()
_employee_seq = 0
_inn_seq = 1000
_INN_COEFFS = (-1, 5, 7, 9, 4, 6, 10, 5, 7)
def next_inn():
"""Валидный 10-значный ИНН, уникальный в пределах прогона тестов."""
global _inn_seq
_inn_seq += 1
body = f'{_inn_seq:09d}'
total = sum(int(body[i]) * _INN_COEFFS[i] for i in range(9))
remainder = total % 11
if remainder < 2:
checksum = 0
elif remainder >= 10:
checksum = remainder - 10
else:
checksum = remainder
return body + str(checksum)
from core.test_utils import login_2fa
def make_employee(**overrides):
global _employee_seq
_employee_seq += 1
defaults = {
'username': f'hiring.hr{_employee_seq}',
'email': f'hiring.hr{_employee_seq}@company.local',
'first_name': 'Ганна',
'last_name': 'HR-Тестова',
'hire_date': date(2026, 1, 1),
'user_location_type': 'office',
'is_staff': False,
'mattermost_sync_enabled': False,
'ad_sync_enabled': False,
}
defaults.update(overrides)
return Employee.objects.create(**defaults)
def make_intern(label='Intern-1', username=None, password='intern-pass'):
return InternAccount.objects.create(
label=label,
username=username or f'intern.{label.lower().replace("-", "")}',
password=password,
is_active=True,
)
class HiringServiceTests(TestCase):
@classmethod
def setUpTestData(cls):
org_type = DirectoryType.objects.create(code='organization', name='Организация')
region_type = DirectoryType.objects.create(code='region', name='Регион')
dept_type = DirectoryType.objects.create(code='department', name='Відділ')
DirectoryType.objects.get_or_create(code='access_system', defaults={'name': 'Системи доступу'})
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одеса')
cls.org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
cls.department = Directory.objects.create(directory_type=dept_type, code='hr', name='HR')
cls.pharmacy = Pharmacy.objects.create(
organization=cls.org, location_region=cls.region,
pharmacy_number=1, location_type='pharmacy',
)
cls.office = Location.objects.create(
location_type='office', location_region=cls.region,
street='Дерибасівська', house_number='1',
)
def setUp(self):
from django.contrib.auth.models import Group
self.hr_user = make_employee()
self.hr_user.groups.add(Group.objects.get(name='HR'))
# Сид-миграция 0005 наполняет пул Intern-1..5 — очищаем, чтобы тесты сами
# управляли составом стажёров (иначе конфликт лейблов и «свободные» из сида).
InternAccount.objects.all().delete()
self.intern = make_intern('Intern-1')
def _pharmacy_kwargs(self, **extra):
defaults = dict(
requester=self.hr_user,
last_name='Кандидат',
first_name='Іван',
user_location_type='pharmacy',
user_pharmacy_id=self.pharmacy.pk,
hire_date=date.today() + timedelta(days=1),
trial_days=3,
inn_code=next_inn(),
)
defaults.update(extra)
return defaults
def _office_kwargs(self, **extra):
defaults = dict(
requester=self.hr_user,
last_name='Офісний',
first_name='Кандидат',
user_location_type='office',
department_id=self.department.pk,
user_location_id=self.office.pk,
hire_date=date.today() + timedelta(days=1),
trial_days=2,
inn_code=next_inn(),
)
defaults.update(extra)
return defaults
def test_inn_required(self):
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._pharmacy_kwargs(inn_code=''))
self.assertIn('inn_code', cm.exception.message_dict)
def test_inn_copied_to_employee_on_approve(self):
inn = next_inn()
hr = create_hiring_request(**self._pharmacy_kwargs(inn_code=inn))
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertEqual(approved.employee.inn_code, inn)
def test_duplicate_inn_on_trial_rejected(self):
inn = next_inn()
create_hiring_request(**self._pharmacy_kwargs(inn_code=inn))
make_intern('Intern-2')
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._pharmacy_kwargs(
last_name='Другий', first_name='Кандидат', inn_code=inn,
))
self.assertIn('inn_code', cm.exception.message_dict)
def test_create_assigns_intern_and_ticket_without_employee(self):
hire_date = date.today() + timedelta(days=1)
hr = create_hiring_request(**self._pharmacy_kwargs(hire_date=hire_date))
self.assertEqual(hr.status, 'trial')
self.assertIsNone(hr.employee)
self.assertIsNotNone(hr.granted_ticket)
self.assertEqual(hr.granted_ticket.ticket_type.code, 'access')
self.assertEqual(hr.granted_ticket.priority.code, 'high')
self.assertEqual(hr.intern_account.pk, self.intern.pk)
self.assertEqual(hr.trial_end_date, hire_date + timedelta(days=3))
self.assertFalse(hr.is_overdue)
self.assertIsNone(hr.access_task)
def test_no_free_intern_raises(self):
create_hiring_request(**self._pharmacy_kwargs())
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._pharmacy_kwargs(
last_name='Другий', first_name='Кандидат',
))
self.assertIn('__all__', cm.exception.message_dict)
def test_pharmacy_without_pharmacy_raises(self):
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._pharmacy_kwargs(user_pharmacy_id=None))
self.assertIn('user_pharmacy', cm.exception.message_dict)
def test_office_without_department_raises(self):
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._office_kwargs(department_id=None))
self.assertIn('department', cm.exception.message_dict)
def test_office_without_location_raises(self):
with self.assertRaises(ValidationError) as cm:
create_hiring_request(**self._office_kwargs(user_location_id=None))
self.assertIn('user_location', cm.exception.message_dict)
def test_approve_assigns_default_groups_by_location(self):
from django.contrib.auth.models import Group
from apps.employees.models import EmployeeDefaults
role = Group.objects.create(name='Аптека-базовая')
EmployeeDefaults.load().default_groups_pharmacy.add(role)
hr = create_hiring_request(**self._pharmacy_kwargs())
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertIn(role, approved.employee.groups.all())
def test_approve_does_not_assign_other_location_groups(self):
from django.contrib.auth.models import Group
from apps.employees.models import EmployeeDefaults
pharm_role = Group.objects.create(name='Аптека-роль')
EmployeeDefaults.load().default_groups_pharmacy.add(pharm_role)
hr = create_hiring_request(**self._office_kwargs())
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertNotIn(pharm_role, approved.employee.groups.all())
def test_is_overdue_when_trial_period_passed(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Прострочений', first_name='Тест',
hire_date=date(2026, 1, 1), trial_days=1,
))
self.assertTrue(hr.is_overdue)
def _access_ids(self, n=2):
ids = list(Directory.objects.filter(directory_type__code='access_system', is_active=True)
.values_list('id', flat=True)[:n])
self.assertTrue(ids, 'у тестовій БД немає довідника access_system')
return ids
def test_create_with_access_creates_ticket_not_task(self):
access_ids = self._access_ids(2)
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Доступний', first_name='Кандидат',
requested_access_ids=access_ids,
))
self.assertIsNone(hr.access_task)
self.assertIsNotNone(hr.granted_ticket)
self.assertEqual(hr.requested_access.count(), len(access_ids))
self.assertIn('Доступний', hr.granted_ticket.description)
self.assertEqual(Ticket.objects.filter(pk=hr.granted_ticket_id).count(), 1)
def test_create_without_access_still_creates_ticket(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Без', first_name='Доступів',
))
self.assertIsNone(hr.access_task)
self.assertIsNotNone(hr.granted_ticket)
def test_access_for_office_only_skipped_for_pharmacy(self):
access = Directory.objects.filter(
directory_type__code='access_system', is_active=True,
).first()
self.assertIsNotNone(access)
loc_type = DirectoryType.objects.get(code='location_type')
office_lt = Directory.objects.get_or_create(
directory_type=loc_type, code='office',
defaults={'name': 'Офіс', 'is_active': True},
)[0]
access.location_types.set([office_lt])
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Фільтр', first_name='Доступ',
requested_access_ids=[access.pk],
))
self.assertEqual(hr.requested_access.count(), 0)
make_intern('Intern-2')
hr_office = create_hiring_request(**self._office_kwargs(
last_name='Фільтр', first_name='Офіс',
requested_access_ids=[access.pk],
))
self.assertEqual(list(hr_office.requested_access.values_list('pk', flat=True)), [access.pk])
def test_approve_copies_contract_employment_type(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='ЦПХ', first_name='Кандидат',
employment_type='contract',
))
self.assertEqual(hr.employment_type, 'contract')
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertEqual(approved.employee.employment_type, 'contract')
def test_invalid_employment_type_defaults_to_staff(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Штат', first_name='Кандидат',
employment_type='unknown',
))
self.assertEqual(hr.employment_type, 'staff')
def test_approve_creates_employee_and_frees_intern(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Кандидат', first_name='Марія',
hire_date=date(2026, 6, 1), trial_days=5,
))
ticket = hr.granted_ticket
approved = approve_hiring_request(hr, decided_by=self.hr_user, note='ok')
self.assertEqual(approved.status, 'approved')
self.assertEqual(approved.decision_by, self.hr_user)
self.assertIsNotNone(approved.employee)
self.assertEqual(approved.issued_password, '')
self.assertFalse(approved.employee.has_usable_password())
self.assertEqual(approved.employee.probation_end_date, date.today())
self.intern.refresh_from_db()
self.assertIsNone(self.intern.assigned_to_id)
ticket.refresh_from_db()
self.assertEqual(ticket.status.code, 'closed')
def test_approve_office_assigns_corporate_email(self):
from apps.employees.models import EmployeeDefaults
EmployeeDefaults.load()
hr = create_hiring_request(**self._office_kwargs(
last_name='Петренко', first_name='Іван',
))
approve_hiring_request(hr, decided_by=self.hr_user)
hr.refresh_from_db()
self.assertEqual(
hr.employee.email,
f'{hr.employee.username}@aks-apteka.com.ua',
)
def test_approve_office_uses_admin_domain_setting(self):
from apps.employees.models import EmployeeDefaults
defaults = EmployeeDefaults.load()
defaults.office_email_domain = 'office.example.com'
defaults.save()
hr = create_hiring_request(**self._office_kwargs(
last_name='Сидоренко', first_name='Оля',
))
approve_hiring_request(hr, decided_by=self.hr_user)
hr.refresh_from_db()
self.assertEqual(
hr.employee.email,
f'{hr.employee.username}@office.example.com',
)
def test_approve_pharmacy_uses_placeholder_email(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Аптечна', first_name='Марія',
))
approve_hiring_request(hr, decided_by=self.hr_user)
hr.refresh_from_db()
self.assertTrue(hr.employee.email.endswith('@local.domain'))
def test_approve_does_not_enable_ad_sync(self):
"""HR approve не включает AD sync — учётку в домене создаёт только IT."""
office_hr = create_hiring_request(**self._office_kwargs())
make_intern('Intern-2')
pharmacy_hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Аптечний', first_name='Кандидат',
))
approve_hiring_request(office_hr, decided_by=self.hr_user)
approve_hiring_request(pharmacy_hr, decided_by=self.hr_user)
office_hr.refresh_from_db()
pharmacy_hr.refresh_from_db()
self.assertFalse(office_hr.employee.ad_sync_enabled)
self.assertFalse(pharmacy_hr.employee.ad_sync_enabled)
def test_approve_office_does_not_queue_ad_sync(self):
hr = create_hiring_request(**self._office_kwargs())
with mock.patch('apps.ad_connector.tasks.sync_employee_to_ad_task.delay') as delay:
approve_hiring_request(hr, decided_by=self.hr_user)
delay.assert_not_called()
def test_approve_generates_unique_username(self):
hr1 = create_hiring_request(**self._pharmacy_kwargs(
last_name='Дубль', first_name='Петро', hire_date=date(2026, 6, 1), trial_days=1,
))
make_intern('Intern-2')
hr2 = create_hiring_request(**self._pharmacy_kwargs(
last_name='Дубль', first_name='Петро', hire_date=date(2026, 6, 1), trial_days=1,
))
approve_hiring_request(hr1, decided_by=self.hr_user)
approve_hiring_request(hr2, decided_by=self.hr_user)
hr1.refresh_from_db()
hr2.refresh_from_db()
self.assertNotEqual(hr1.employee.username, hr2.employee.username)
def test_reject_frees_intern_closes_ticket_no_employee(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Кандидат', first_name='Олег',
hire_date=date(2026, 6, 1), trial_days=2,
))
ticket = hr.granted_ticket
rejected = reject_hiring_request(hr, decided_by=self.hr_user, note='не пройшов')
self.assertEqual(rejected.status, 'rejected')
self.assertIsNone(rejected.employee)
self.intern.refresh_from_db()
self.assertIsNone(self.intern.assigned_to_id)
ticket.refresh_from_db()
self.assertEqual(ticket.status.code, 'closed')
def test_decision_is_a_noop_once_already_decided(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Кандидат', first_name='Ліда',
hire_date=date(2026, 6, 1), trial_days=2,
))
approve_hiring_request(hr, decided_by=self.hr_user)
reject_hiring_request(hr, decided_by=self.hr_user)
hr.refresh_from_db()
self.assertEqual(hr.status, 'approved')
def test_credentials_pdf_after_approve(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='PDF', first_name='Тест',
))
approve_hiring_request(hr, decided_by=self.hr_user)
client = Client()
login_2fa(client, self.hr_user)
url = reverse('hiring_credentials_pdf', kwargs={'pk': hr.pk})
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/pdf')
self.assertTrue(response.content.startswith(b'%PDF'))
hr.refresh_from_db()
self.assertEqual(hr.issued_password, '')
second = client.get(url)
self.assertEqual(second.status_code, 200)
def test_credentials_pdf_forbidden_while_trial(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='PDF', first_name='Trial',
))
client = Client()
login_2fa(client, self.hr_user)
response = client.get(reverse('hiring_credentials_pdf', kwargs={'pk': hr.pk}))
self.assertEqual(response.status_code, 403)
def test_hr_portal_shows_hiring_ui_without_staff(self):
client = Client()
login_2fa(client, self.hr_user)
response = client.get('/hr-portal/', HTTP_HX_REQUEST='true')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context['can_manage_hiring'])
self.assertContains(response, 'Новая заявка на найм')
self.assertContains(response, 'id="hr-hiring-modal"')
self.assertContains(response, 'data-step="3"')