b2f0c2a1e2
Ранее разделы с perm=None (Новости, Задачи, Дислокация, Документы, Чат, Календарь, HR-портал, Справка) были доступны любому вошедшему — сотрудник без назначенной группы видел почти весь портал. Теперь такие разделы требуют хотя бы одной группы; без групп доступны только базовые: Главная, Профиль, Поддержка IT. - user_can_access: BASELINE_OPEN_KEYS + gate «есть ли группа» для perm=None - PortalSectionAccessMiddleware: барьер по префиксу URL (в т.ч. вложенные страницы вроде /tasks/5/), /settings/ не гейтим (редирект на свой профиль) - index.html: пункты меню под can_section (сайдбар, дропдаун, bottom-nav), секция «Управление» скрывается целиком, если пусто - тесты: политика без-группы + прямой заход по URL; тест-фабрики выдают группу (реальный сотрудник её имеет) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
338 lines
16 KiB
Python
338 lines
16 KiB
Python
from datetime import date
|
||
|
||
from django.contrib.auth.models import Group, Permission
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.test import TestCase
|
||
|
||
from apps.employees.models import Employee, EmployeePermissionOverride
|
||
from core.models import PortalAccess
|
||
from core.portal_access import DASHBOARD_PATH, user_can_access, user_can_manage_hr_portal
|
||
from core.accounting_portal import user_can_access_accounting_portal
|
||
|
||
|
||
def make_employee(**overrides):
|
||
n = Employee.objects.count() + 1
|
||
defaults = {
|
||
'username': f'portal.user{n}',
|
||
'email': f'portal.user{n}@company.local',
|
||
'first_name': 'Тест',
|
||
'last_name': 'Портал',
|
||
'hire_date': date(2026, 1, 1),
|
||
'user_location_type': 'pharmacy',
|
||
'mattermost_sync_enabled': False,
|
||
'ad_sync_enabled': False,
|
||
'is_staff': False,
|
||
}
|
||
defaults.update(overrides)
|
||
return Employee.objects.create(**defaults)
|
||
|
||
|
||
def assert_denied_to_dashboard(test, response, msg=None):
|
||
test.assertEqual(response.status_code, 302, msg)
|
||
test.assertEqual(response['Location'], DASHBOARD_PATH, msg)
|
||
if response.has_header('HX-Redirect'):
|
||
test.assertEqual(response['HX-Redirect'], DASHBOARD_PATH, msg)
|
||
|
||
|
||
from core.test_utils import login_2fa
|
||
|
||
|
||
def portal_perm(codename: str) -> Permission:
|
||
ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False)
|
||
return Permission.objects.get(content_type=ct, codename=codename)
|
||
|
||
|
||
class PortalAccessUnitTests(TestCase):
|
||
def test_baseline_sections_without_group(self):
|
||
user = make_employee()
|
||
# Базовые разделы видны даже без назначенных групп.
|
||
self.assertTrue(user_can_access(user, 'dashboard'))
|
||
self.assertTrue(user_can_access(user, 'profile'))
|
||
self.assertTrue(user_can_access(user, 'it_support'))
|
||
# Прочие «открытые» разделы теперь требуют хотя бы одной группы.
|
||
self.assertFalse(user_can_access(user, 'dislocation'))
|
||
self.assertFalse(user_can_access(user, 'news'))
|
||
self.assertFalse(user_can_access(user, 'tasks'))
|
||
self.assertFalse(user_can_access(user, 'help'))
|
||
self.assertFalse(user_can_access(user, 'rro'))
|
||
|
||
def test_open_sections_visible_with_any_group(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.create(name='Пустая роль'))
|
||
self.assertTrue(user_can_access(user, 'dislocation'))
|
||
self.assertTrue(user_can_access(user, 'news'))
|
||
self.assertTrue(user_can_access(user, 'tasks'))
|
||
self.assertTrue(user_can_access(user, 'help'))
|
||
|
||
def test_hr_portal_requires_group(self):
|
||
user = make_employee()
|
||
# Без группы HR-портал не виден.
|
||
self.assertFalse(user_can_access(user, 'hr_portal'))
|
||
self.assertFalse(user_can_manage_hr_portal(user))
|
||
user.groups.add(Group.objects.create(name='Пустая роль'))
|
||
# С группой раздел виден, но управление наймом всё ещё недоступно.
|
||
self.assertTrue(user_can_access(user, 'hr_portal'))
|
||
self.assertFalse(user_can_manage_hr_portal(user))
|
||
|
||
def test_accounting_portal_requires_subsection(self):
|
||
user = make_employee()
|
||
self.assertFalse(user_can_access_accounting_portal(user))
|
||
user.groups.add(Group.objects.get(name='Фармацевт'))
|
||
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
|
||
if hasattr(user, attr):
|
||
delattr(user, attr)
|
||
self.assertTrue(user_can_access_accounting_portal(user))
|
||
|
||
def test_office_has_internet_billing_perm(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
|
||
if hasattr(user, attr):
|
||
delattr(user, attr)
|
||
self.assertTrue(user_can_access(user, 'internet_billing'))
|
||
|
||
def test_restricted_denied_without_perm(self):
|
||
user = make_employee()
|
||
self.assertFalse(user_can_access(user, 'it_manager'))
|
||
|
||
def test_staff_without_perm_denied(self):
|
||
user = make_employee(is_staff=True)
|
||
self.assertFalse(user_can_manage_hr_portal(user))
|
||
self.assertFalse(user_can_access(user, 'databases'))
|
||
|
||
def test_hr_group_manage_perm(self):
|
||
user = make_employee()
|
||
hr = Group.objects.get(name='HR')
|
||
user.groups.add(hr)
|
||
# clear cached perms
|
||
if hasattr(user, '_perm_cache'):
|
||
del user._perm_cache
|
||
if hasattr(user, '_user_perm_cache'):
|
||
del user._user_perm_cache
|
||
if hasattr(user, '_group_perm_cache'):
|
||
del user._group_perm_cache
|
||
self.assertTrue(user_can_access(user, 'hr_portal'))
|
||
self.assertTrue(user_can_manage_hr_portal(user))
|
||
self.assertTrue(user_can_access(user, 'recruiting'))
|
||
self.assertFalse(user_can_access(user, 'it_manager'))
|
||
|
||
def test_override_grant(self):
|
||
user = make_employee()
|
||
EmployeePermissionOverride.objects.create(
|
||
employee=user,
|
||
permission=portal_perm('access_hr_portal'),
|
||
override_type='grant',
|
||
)
|
||
self.assertTrue(user_can_manage_hr_portal(user))
|
||
|
||
def test_override_revoke(self):
|
||
user = make_employee()
|
||
it = Group.objects.get(name='IT')
|
||
user.groups.add(it)
|
||
if hasattr(user, '_perm_cache'):
|
||
del user._perm_cache
|
||
self.assertTrue(user_can_access(user, 'it_manager'))
|
||
EmployeePermissionOverride.objects.create(
|
||
employee=user,
|
||
permission=portal_perm('access_it_manager'),
|
||
override_type='revoke',
|
||
)
|
||
self.assertFalse(user_can_access(user, 'it_manager'))
|
||
|
||
|
||
class PortalAccessViewTests(TestCase):
|
||
def test_pharmacist_open_ok_restricted_forbidden(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.create(name='Пустая роль'))
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/dashboard/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/dislocation/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/help/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
assert_denied_to_dashboard(self, self.client.get('/rro/', HTTP_HX_REQUEST='true'))
|
||
assert_denied_to_dashboard(self, self.client.get('/pharmacies/', HTTP_HX_REQUEST='true'))
|
||
assert_denied_to_dashboard(self, self.client.get('/utilities/', HTTP_HX_REQUEST='true'))
|
||
self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
assert_denied_to_dashboard(self, self.client.get('/it-manager/', HTTP_HX_REQUEST='true'))
|
||
assert_denied_to_dashboard(self, self.client.get('/databases/', HTTP_HX_REQUEST='true'))
|
||
|
||
def test_pharmacist_hr_portal_read_only(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.create(name='Пустая роль'))
|
||
login_2fa(self.client, user)
|
||
response = self.client.get('/hr-portal/', HTTP_HX_REQUEST='true')
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertFalse(response.context['can_manage_hiring'])
|
||
|
||
def test_pharmacist_cannot_create_hiring(self):
|
||
user = make_employee()
|
||
login_2fa(self.client, user)
|
||
response = self.client.post('/hiring/', {'last_name': 'Test'}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(response.status_code, 403)
|
||
|
||
def test_employees_list_redirects_to_hr_portal(self):
|
||
user = make_employee()
|
||
login_2fa(self.client, user)
|
||
response = self.client.get('/employees/')
|
||
self.assertEqual(response.status_code, 302)
|
||
self.assertEqual(response['Location'], '/hr-portal/')
|
||
|
||
def test_hr_group_can_open_hr_portal(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='HR'))
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
|
||
def test_it_group_can_open_it_manager(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='IT'))
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/it-manager/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
|
||
def test_staff_without_perm_views_forbidden(self):
|
||
user = make_employee(is_staff=True, totp_enabled=True)
|
||
login_2fa(self.client, user)
|
||
assert_denied_to_dashboard(self, self.client.get('/databases/', HTTP_HX_REQUEST='true'))
|
||
assert_denied_to_dashboard(self, self.client.get('/ad-mapping/', HTTP_HX_REQUEST='true'))
|
||
|
||
def test_it_group_can_open_databases_and_ad(self):
|
||
user = make_employee(totp_enabled=True)
|
||
user.groups.add(Group.objects.get(name='IT'))
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/databases/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/ad-mapping/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
|
||
def test_manage_sections_require_permission(self):
|
||
user = make_employee()
|
||
login_2fa(self.client, user)
|
||
for url in ('/pharmacies/', '/rro/', '/utilities/', '/structure/', '/timetrack/'):
|
||
assert_denied_to_dashboard(self, self.client.get(url, HTTP_HX_REQUEST='true'), url)
|
||
|
||
def test_office_group_manage_sections(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
login_2fa(self.client, user)
|
||
for url in ('/pharmacies/', '/utilities/', '/structure/', '/timetrack/', '/accounting/'):
|
||
self.assertEqual(self.client.get(url, HTTP_HX_REQUEST='true').status_code, 200, url)
|
||
assert_denied_to_dashboard(self, self.client.get('/rro/', HTTP_HX_REQUEST='true'))
|
||
# /recruiting/ всегда редиректит на вкладку вакансий HR
|
||
self.assertEqual(self.client.get('/recruiting/', HTTP_HX_REQUEST='true').status_code, 302)
|
||
|
||
def test_accounting_portal_forbidden_without_subsection_perm(self):
|
||
user = make_employee()
|
||
login_2fa(self.client, user)
|
||
assert_denied_to_dashboard(self, self.client.get('/accounting/', HTTP_HX_REQUEST='true'))
|
||
|
||
def test_accounting_portal_shows_available_tabs(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
login_2fa(self.client, user)
|
||
response = self.client.get('/accounting/', HTTP_HX_REQUEST='true')
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response.context['accounting_active_tab'], 'utilities')
|
||
tab_keys = [t[0] for t in response.context['accounting_tabs']]
|
||
self.assertNotIn('rro', tab_keys)
|
||
self.assertIn('utilities', tab_keys)
|
||
self.assertIn('power', tab_keys)
|
||
self.assertIn('timetrack', tab_keys)
|
||
|
||
def test_accounting_portal_tab_param(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
login_2fa(self.client, user)
|
||
response = self.client.get('/accounting/?tab=timetrack', HTTP_HX_REQUEST='true')
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response.context['accounting_active_tab'], 'timetrack')
|
||
|
||
def test_office_rro_via_override(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
EmployeePermissionOverride.objects.create(
|
||
employee=user,
|
||
permission=portal_perm('access_rro'),
|
||
override_type='grant',
|
||
)
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/rro/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
|
||
def test_manager_group_rro_and_utilities(self):
|
||
user = make_employee(user_location_type='pharmacy')
|
||
user.groups.add(Group.objects.get(name='Заведующий аптекой'))
|
||
login_2fa(self.client, user)
|
||
self.assertEqual(self.client.get('/rro/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/utilities/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/pharmacies/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
|
||
def test_stub_sections_staff_only(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
|
||
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
|
||
if hasattr(user, attr):
|
||
delattr(user, attr)
|
||
self.assertFalse(user_can_access(user, 'purchases'))
|
||
self.assertTrue(user_can_access(user, 'help'))
|
||
|
||
staff = make_employee(is_staff=True, user_location_type='office')
|
||
self.assertTrue(user_can_access(staff, 'purchases'))
|
||
# help — обычный «открытый» раздел: без группы не виден даже staff.
|
||
self.assertFalse(user_can_access(staff, 'help'))
|
||
|
||
|
||
class NoGroupSectionAccessTests(TestCase):
|
||
"""Сотрудник без групп: только базовые разделы, прочее — редирект."""
|
||
|
||
def test_no_group_blocked_from_open_sections(self):
|
||
user = make_employee()
|
||
login_2fa(self.client, user)
|
||
# базовые — доступны
|
||
self.assertEqual(self.client.get('/dashboard/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
self.assertEqual(self.client.get('/it-support/', HTTP_HX_REQUEST='true').status_code, 200)
|
||
# ранее открытые — теперь редирект на дашборд
|
||
for url in ('/news/', '/tasks/', '/documents/', '/chat/',
|
||
'/calendar/', '/dislocation/', '/hr-portal/', '/help/'):
|
||
assert_denied_to_dashboard(self, self.client.get(url, HTTP_HX_REQUEST='true'), url)
|
||
# /settings/ не блокируется — редирект на свой профиль (2FA/пароль).
|
||
settings_resp = self.client.get('/settings/')
|
||
self.assertEqual(settings_resp.status_code, 302)
|
||
self.assertIn(f'/employees/{user.pk}/?tab=settings', settings_resp['Location'])
|
||
|
||
def test_group_opens_sections(self):
|
||
user = make_employee()
|
||
user.groups.add(Group.objects.create(name='Пустая роль'))
|
||
login_2fa(self.client, user)
|
||
for url in ('/news/', '/tasks/', '/dislocation/', '/hr-portal/', '/help/'):
|
||
self.assertEqual(
|
||
self.client.get(url, HTTP_HX_REQUEST='true').status_code, 200, url,
|
||
)
|
||
|
||
|
||
class AccessPreviewTests(TestCase):
|
||
def test_preview_limits_superuser_like_pharmacist(self):
|
||
from django.test import override_settings
|
||
from core.access_preview import attach_preview_to_user
|
||
|
||
admin = make_employee(is_staff=True, is_superuser=True, user_location_type='office')
|
||
self.assertTrue(user_can_access(admin, 'it_manager'))
|
||
|
||
with override_settings(DEBUG=True):
|
||
attach_preview_to_user(admin, 'pharmacist')
|
||
self.assertFalse(user_can_access(admin, 'it_manager'))
|
||
self.assertFalse(user_can_access(admin, 'databases'))
|
||
self.assertTrue(user_can_access(admin, 'dashboard'))
|
||
self.assertTrue(user_can_access(admin, 'utilities'))
|
||
attach_preview_to_user(admin, '')
|
||
self.assertTrue(user_can_access(admin, 'it_manager'))
|
||
|
||
def test_preview_endpoint_sets_session(self):
|
||
from django.test import override_settings
|
||
|
||
admin = make_employee(is_staff=True, is_superuser=True, user_location_type='office')
|
||
with override_settings(DEBUG=True):
|
||
login_2fa(self.client, admin)
|
||
resp = self.client.post('/dev/access-preview/', {'preview': 'it', 'next': '/dashboard/'})
|
||
self.assertEqual(resp.status_code, 302)
|
||
self.assertEqual(self.client.session.get('dev_access_preview'), 'it')
|
||
# В preview staff-only заглушки недоступны → редирект на главную
|
||
assert_denied_to_dashboard(
|
||
self, self.client.get('/purchases/', HTTP_HX_REQUEST='true'),
|
||
)
|