Files
ACORE/apps/employees/tests_password_invite.py
T

299 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)