7b04ca7477
Co-authored-by: Cursor <cursoragent@cursor.com>
299 lines
13 KiB
Python
299 lines
13 KiB
Python
from datetime import date
|
||
from unittest import mock
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from django.test import Client, TestCase
|
||
from django.urls import reverse
|
||
|
||
from apps.employees.models import PasswordSetupInvite
|
||
from apps.employees.services.password_invite import (
|
||
can_offer_ad_password_sync,
|
||
complete_password_setup,
|
||
create_password_invite,
|
||
get_active_invite,
|
||
)
|
||
from apps.hiring.models import InternAccount
|
||
from apps.hiring.services import approve_hiring_request, create_hiring_request
|
||
from core.test_utils import login_2fa
|
||
|
||
Employee = get_user_model()
|
||
|
||
|
||
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 PasswordInviteUnitTests(TestCase):
|
||
def setUp(self):
|
||
self.employee = Employee.objects.create(
|
||
username='invite.test',
|
||
email='invite.test@aks-apteka.com.ua',
|
||
first_name='Тест',
|
||
last_name='Запрошення',
|
||
hire_date=date(2026, 1, 1),
|
||
user_location_type='office',
|
||
mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False,
|
||
)
|
||
self.employee.set_unusable_password()
|
||
self.employee.save()
|
||
|
||
def test_create_and_resolve_invite(self):
|
||
invite = create_password_invite(self.employee)
|
||
resolved = get_active_invite(invite.token)
|
||
self.assertEqual(resolved.pk, invite.pk)
|
||
|
||
def test_complete_sets_password(self):
|
||
invite = create_password_invite(self.employee)
|
||
complete_password_setup(invite, 'GoodPass123!')
|
||
self.employee.refresh_from_db()
|
||
self.assertTrue(self.employee.check_password('GoodPass123!'))
|
||
invite.refresh_from_db()
|
||
self.assertIsNotNone(invite.used_at)
|
||
|
||
def test_ad_checkbox_only_with_dn(self):
|
||
self.assertFalse(can_offer_ad_password_sync(self.employee))
|
||
self.employee.ad_user_dn = 'CN=Test,OU=Users,DC=local'
|
||
self.assertTrue(can_offer_ad_password_sync(self.employee))
|
||
|
||
@mock.patch('apps.ad_connector.services.sync_service.sync_ad_password', return_value=True)
|
||
def test_sync_ad_on_setup(self, sync_pw):
|
||
self.employee.ad_user_dn = 'CN=Test,OU=Users,DC=local'
|
||
self.employee.save()
|
||
invite = create_password_invite(self.employee)
|
||
complete_password_setup(invite, 'GoodPass123!', sync_ad=True)
|
||
self.employee.refresh_from_db()
|
||
self.assertTrue(self.employee.ad_sync_enabled)
|
||
sync_pw.assert_called_once()
|
||
|
||
|
||
class PasswordSetupViewTests(TestCase):
|
||
def setUp(self):
|
||
self.employee = Employee.objects.create(
|
||
username='portal.setup',
|
||
email='portal.setup@local.domain',
|
||
first_name='Портал',
|
||
last_name='Setup',
|
||
hire_date=date(2026, 1, 1),
|
||
user_location_type='pharmacy',
|
||
mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False,
|
||
)
|
||
self.employee.set_unusable_password()
|
||
self.employee.save()
|
||
self.invite = create_password_invite(self.employee)
|
||
self.client = Client()
|
||
|
||
def test_setup_form_and_login_redirect(self):
|
||
url = reverse('password_setup', kwargs={'token': self.invite.token})
|
||
response = self.client.get(url)
|
||
self.assertEqual(response.status_code, 200)
|
||
response = self.client.post(url, {
|
||
'password': 'SecurePass123',
|
||
'password_confirm': 'SecurePass123',
|
||
})
|
||
self.assertEqual(response.status_code, 302)
|
||
self.assertIn('/login/', response['Location'])
|
||
self.employee.refresh_from_db()
|
||
self.assertTrue(self.employee.check_password('SecurePass123'))
|
||
|
||
|
||
class BulkInviteAndMattermostTests(TestCase):
|
||
def _emp(self, username, *, with_password=False, mm_id=''):
|
||
emp = Employee.objects.create(
|
||
username=username, email=f'{username}@company.local',
|
||
first_name='И', last_name='Ф', hire_date=date(2026, 1, 1),
|
||
user_location_type='office', mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False, mattermost_user_id=mm_id,
|
||
)
|
||
if with_password:
|
||
emp.set_password('Existing123!')
|
||
else:
|
||
emp.set_unusable_password()
|
||
emp.save()
|
||
return emp
|
||
|
||
def test_bulk_creates_only_for_pending(self):
|
||
from apps.employees.services.password_invite import (
|
||
bulk_create_invites_for_pending, create_password_invite,
|
||
)
|
||
pending = self._emp('bulk.pending')
|
||
has_pw = self._emp('bulk.haspw', with_password=True)
|
||
already = self._emp('bulk.already')
|
||
create_password_invite(already) # уже есть активная ссылка
|
||
|
||
n = bulk_create_invites_for_pending()
|
||
self.assertEqual(n, 1)
|
||
self.assertTrue(PasswordSetupInvite.objects.filter(employee=pending, used_at__isnull=True).exists())
|
||
self.assertFalse(PasswordSetupInvite.objects.filter(employee=has_pw).exists())
|
||
# у already осталась одна активная (не задублировали)
|
||
self.assertEqual(PasswordSetupInvite.objects.filter(employee=already, used_at__isnull=True).count(), 1)
|
||
|
||
def test_revoke_active_invites(self):
|
||
from apps.employees.services.password_invite import (
|
||
create_password_invite, get_active_invite_for_employee, revoke_active_invites,
|
||
)
|
||
emp = self._emp('bulk.revoke')
|
||
create_password_invite(emp)
|
||
self.assertIsNotNone(get_active_invite_for_employee(emp))
|
||
revoke_active_invites(emp)
|
||
self.assertIsNone(get_active_invite_for_employee(emp))
|
||
|
||
def test_link_and_set_password_ok(self):
|
||
emp = self._emp('mm.link')
|
||
path = 'apps.mattermost_connector.services.sync_service'
|
||
|
||
def fake_sync_employee(employee, password=None):
|
||
employee.mattermost_user_id = 'mm-123'
|
||
return True
|
||
|
||
with mock.patch(f'{path}.MattermostService'), \
|
||
mock.patch(f'{path}.sync_employee', side_effect=fake_sync_employee), \
|
||
mock.patch(f'{path}.sync_password', return_value=True) as sp:
|
||
from apps.mattermost_connector.services.sync_service import link_and_set_password
|
||
status = link_and_set_password(emp, 'NewPass123!')
|
||
self.assertEqual(status, 'ok')
|
||
sp.assert_called_once()
|
||
emp.refresh_from_db()
|
||
self.assertTrue(emp.mattermost_sync_enabled)
|
||
|
||
def test_link_and_set_password_disabled_when_no_config(self):
|
||
from apps.mattermost_connector.mattermost_service import MattermostConfigError
|
||
emp = self._emp('mm.noconf')
|
||
path = 'apps.mattermost_connector.services.sync_service'
|
||
with mock.patch(f'{path}.MattermostService', side_effect=MattermostConfigError('нет конфига')):
|
||
from apps.mattermost_connector.services.sync_service import link_and_set_password
|
||
status = link_and_set_password(emp, 'NewPass123!')
|
||
self.assertEqual(status, 'disabled')
|
||
|
||
def test_activation_triggers_mattermost_sync(self):
|
||
from apps.employees.services.password_invite import create_password_invite
|
||
emp = self._emp('mm.activate')
|
||
invite = create_password_invite(emp)
|
||
url = reverse('password_setup', kwargs={'token': invite.token})
|
||
with mock.patch(
|
||
'apps.mattermost_connector.services.sync_service.link_and_set_password',
|
||
return_value='ok',
|
||
) as link:
|
||
resp = Client().post(url, {'password': 'SecurePass123', 'password_confirm': 'SecurePass123'})
|
||
self.assertEqual(resp.status_code, 302)
|
||
link.assert_called_once()
|
||
self.assertEqual(link.call_args.args[0].pk, emp.pk)
|
||
self.assertEqual(link.call_args.args[1], 'SecurePass123')
|
||
|
||
|
||
class PasswordInviteAdminPageTests(TestCase):
|
||
def setUp(self):
|
||
self.admin = Employee.objects.create(
|
||
username='inv.admin', email='inv.admin@company.local',
|
||
first_name='Адм', last_name='Ин', hire_date=date(2026, 1, 1),
|
||
user_location_type='office', is_staff=True, is_superuser=True,
|
||
mattermost_sync_enabled=False, ad_sync_enabled=False,
|
||
)
|
||
self.admin.set_password('AdminPass123!')
|
||
self.admin.save()
|
||
self.pending = Employee.objects.create(
|
||
username='inv.pending', email='inv.pending@company.local',
|
||
first_name='П', last_name='К', hire_date=date(2026, 1, 1),
|
||
user_location_type='office', mattermost_sync_enabled=False, ad_sync_enabled=False,
|
||
)
|
||
self.pending.set_unusable_password()
|
||
self.pending.save()
|
||
login_2fa(self.client, self.admin)
|
||
self.url = reverse('admin:index') + 'employees/employee/password-invites/'
|
||
|
||
def test_page_renders(self):
|
||
resp = self.client.get(self.url)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.assertContains(resp, 'inv.pending')
|
||
|
||
def test_generate_all_creates_invites(self):
|
||
resp = self.client.post(self.url, {'action': 'generate_all'})
|
||
self.assertEqual(resp.status_code, 302)
|
||
self.assertTrue(PasswordSetupInvite.objects.filter(employee=self.pending, used_at__isnull=True).exists())
|
||
|
||
def test_generate_and_revoke_one(self):
|
||
self.client.post(self.url, {'action': 'generate_one', 'employee_id': self.pending.pk})
|
||
self.assertEqual(PasswordSetupInvite.objects.filter(employee=self.pending, used_at__isnull=True).count(), 1)
|
||
self.client.post(self.url, {'action': 'revoke_one', 'employee_id': self.pending.pk})
|
||
self.assertEqual(PasswordSetupInvite.objects.filter(employee=self.pending, used_at__isnull=True).count(), 0)
|
||
|
||
def test_csv_export(self):
|
||
self.client.post(self.url, {'action': 'generate_one', 'employee_id': self.pending.pk})
|
||
resp = self.client.get(self.url + '?export=csv')
|
||
self.assertEqual(resp['Content-Type'].split(';')[0], 'text/csv')
|
||
self.assertIn('inv.pending', resp.content.decode('utf-8'))
|
||
|
||
|
||
class HiringInviteFlowTests(TestCase):
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
from django.contrib.auth.models import Group
|
||
from core.models import Directory, DirectoryType
|
||
from apps.locations.models import Pharmacy
|
||
|
||
org_type, _ = DirectoryType.objects.get_or_create(code='organization', defaults={'name': 'Организация'})
|
||
region_type, _ = DirectoryType.objects.get_or_create(code='region', defaults={'name': 'Регион'})
|
||
dept_type, _ = DirectoryType.objects.get_or_create(code='department', defaults={'name': 'Відділ'})
|
||
post_type, _ = DirectoryType.objects.get_or_create(code='post', defaults={'name': 'Post'})
|
||
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.dept = Directory.objects.create(directory_type=dept_type, code='hr', name='HR')
|
||
cls.post = Directory.objects.create(directory_type=post_type, name='Clerk')
|
||
cls.pharmacy = Pharmacy.objects.create(
|
||
organization=cls.org, location_region=cls.region,
|
||
pharmacy_number=1, location_type='pharmacy',
|
||
)
|
||
cls.hr = Employee.objects.create(
|
||
username='hr.invite.flow',
|
||
email='hr.invite.flow@company.local',
|
||
first_name='HR',
|
||
last_name='Flow',
|
||
hire_date=date(2026, 1, 1),
|
||
user_location_type='office',
|
||
mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False,
|
||
)
|
||
cls.hr.groups.add(Group.objects.get(name='HR'))
|
||
|
||
def setUp(self):
|
||
InternAccount.objects.all().delete()
|
||
make_intern('Intern-1')
|
||
|
||
def _create_trial(self):
|
||
return create_hiring_request(
|
||
requester=self.hr,
|
||
last_name='Новий',
|
||
first_name='Співробітник',
|
||
inn_code='0000000017',
|
||
user_location_type='pharmacy',
|
||
user_pharmacy_id=self.pharmacy.pk,
|
||
department_id=self.dept.pk,
|
||
post_id=self.post.pk,
|
||
hire_date=date(2026, 8, 1),
|
||
trial_days=3,
|
||
)
|
||
|
||
def test_approve_creates_invite_not_password(self):
|
||
hr = self._create_trial()
|
||
approved = approve_hiring_request(hr, decided_by=self.hr)
|
||
self.assertEqual(approved.issued_password, '')
|
||
self.assertFalse(approved.employee.has_usable_password())
|
||
self.assertTrue(
|
||
PasswordSetupInvite.objects.filter(employee=approved.employee, used_at__isnull=True).exists()
|
||
)
|
||
|
||
def test_credentials_pdf_repeatable(self):
|
||
hr = self._create_trial()
|
||
approve_hiring_request(hr, decided_by=self.hr)
|
||
client = Client()
|
||
login_2fa(client, self.hr)
|
||
url = reverse('hiring_credentials_pdf', kwargs={'pk': hr.pk})
|
||
self.assertEqual(client.get(url).status_code, 200)
|
||
self.assertEqual(client.get(url).status_code, 200)
|