9c8d0d109a
Сотрудник может подключить несколько IMAP-ящиков и переключаться между ними. Дислокация снова показывает всю сеть, а не только назначенные аптеки. Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
10 KiB
Python
186 lines
10 KiB
Python
from datetime import date
|
|
from unittest.mock import patch
|
|
|
|
from django.contrib.auth.models import Group, Permission
|
|
from django.test import TestCase, RequestFactory
|
|
from django.urls import reverse
|
|
|
|
from apps.employees.models import Employee
|
|
from apps.locations.models import Pharmacy, GroupLocationAccess, PharmacyCardVisibility
|
|
from core.models import Directory, DirectoryType
|
|
from core.location_access import accessible_pharmacies, accessible_locations, has_all_locations
|
|
from core.test_utils import login_2fa
|
|
|
|
|
|
class LocationAccessTests(TestCase):
|
|
@staticmethod
|
|
def directory(kind, code):
|
|
dtype, _ = DirectoryType.objects.get_or_create(code=kind, defaults={'name': kind})
|
|
return Directory.objects.get_or_create(directory_type=dtype, code=code, defaults={'name': code})[0]
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
directory = cls.directory
|
|
org, region = directory('organization', 'scope-org'), directory('region', 'scope-region')
|
|
cls.own = Pharmacy.objects.create(organization=org, location_region=region, pharmacy_number=701, location_type='pharmacy')
|
|
cls.assigned = Pharmacy.objects.create(organization=org, location_region=region, pharmacy_number=702, location_type='pharmacy')
|
|
cls.foreign = Pharmacy.objects.create(organization=org, location_region=region, pharmacy_number=703, location_type='pharmacy')
|
|
cls.user = Employee.objects.create(username='scoped', email='scope@test.local', hire_date=date(2026, 1, 1),
|
|
user_location_type='pharmacy', user_pharmacy=cls.own, is_staff=True,
|
|
ad_sync_enabled=False, mattermost_sync_enabled=False)
|
|
cls.group = Group.objects.create(name='Scoped operators')
|
|
cls.group.permissions.add(*Permission.objects.filter(codename__in=[
|
|
'access_pharmacies', 'access_utilities', 'access_rro', 'access_it_manager', 'access_internet_billing', 'access_reports']))
|
|
cls.user.groups.add(cls.group)
|
|
# QuerySet update avoids unrelated provisioning hooks/position validation.
|
|
Pharmacy.objects.filter(pk=cls.assigned.pk).update(curator=cls.user)
|
|
|
|
def setUp(self):
|
|
self.user = Employee.objects.get(pk=self.user.pk)
|
|
login_2fa(self.client, self.user)
|
|
|
|
def request(self, path='/'):
|
|
req = RequestFactory().get(path)
|
|
req.user = self.user
|
|
return req
|
|
|
|
def test_staff_is_not_global_and_curator_gets_assigned(self):
|
|
self.assertFalse(has_all_locations(self.user))
|
|
self.assertSetEqual(set(accessible_pharmacies(self.user).values_list('pk', flat=True)), {self.own.pk, self.assigned.pk})
|
|
self.assertSetEqual(set(accessible_locations(self.user).values_list('pk', flat=True)), {self.own.pk, self.assigned.pk})
|
|
|
|
def test_head_and_responsible_manager_assignments(self):
|
|
for field in ('head_pharmacist', 'responsible_manager'):
|
|
Pharmacy.objects.filter(pk=self.foreign.pk).update(**{field: self.user})
|
|
self.assertTrue(accessible_pharmacies(self.user).filter(pk=self.foreign.pk).exists())
|
|
Pharmacy.objects.filter(pk=self.foreign.pk).update(**{field: None})
|
|
self.assertFalse(accessible_pharmacies(self.user).filter(pk=self.foreign.pk).exists())
|
|
|
|
def test_group_assignments_are_additive_and_revocable(self):
|
|
scope = GroupLocationAccess.objects.create(group=self.group)
|
|
scope.locations.add(self.foreign)
|
|
self.assertTrue(accessible_pharmacies(self.user).filter(pk=self.foreign.pk).exists())
|
|
scope.locations.clear()
|
|
self.assertFalse(accessible_pharmacies(self.user).filter(pk=self.foreign.pk).exists())
|
|
|
|
def test_global_permission_is_explicit(self):
|
|
self.user.user_permissions.add(Permission.objects.get(content_type__app_label='locations', codename='view_all_locations'))
|
|
self.user = Employee.objects.get(pk=self.user.pk)
|
|
self.assertTrue(has_all_locations(self.user))
|
|
self.assertEqual(accessible_pharmacies(self.user).count(), 3)
|
|
|
|
def test_no_assignment_and_inactive_fail_closed(self):
|
|
Employee.objects.filter(pk=self.user.pk).update(user_pharmacy=None)
|
|
Pharmacy.objects.filter(curator=self.user).update(curator=None)
|
|
user = Employee.objects.get(pk=self.user.pk)
|
|
self.assertFalse(accessible_pharmacies(user).exists())
|
|
user.is_superuser = True
|
|
user.is_active = False
|
|
self.assertFalse(has_all_locations(user))
|
|
self.assertFalse(accessible_locations(user).exists())
|
|
|
|
def test_preview_does_not_inherit_admin_locations(self):
|
|
self.user.is_superuser = True
|
|
self.user._access_preview_perms = set()
|
|
self.assertFalse(accessible_locations(self.user).exists())
|
|
|
|
def test_lists_and_direct_urls_use_same_scope(self):
|
|
for htmx in ('', 'true'):
|
|
response = self.client.get('/pharmacies/', HTTP_HX_REQUEST=htmx)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, f'/pharmacies/{self.foreign.pk}/')
|
|
# Дислокация — открытый справочник сети: все аптеки видны.
|
|
dislo = self.client.get('/dislocation/', HTTP_HX_REQUEST=htmx)
|
|
self.assertEqual(dislo.status_code, 200)
|
|
self.assertContains(dislo, f'#{self.foreign.pharmacy_number}')
|
|
self.assertEqual(self.client.get(f'/pharmacies/{self.own.pk}/', HTTP_HX_REQUEST=htmx).status_code, 200)
|
|
self.assertEqual(self.client.get(f'/pharmacies/{self.foreign.pk}/', HTTP_HX_REQUEST=htmx).status_code, 404)
|
|
self.assertEqual(self.client.post(reverse('pharmacy_update', args=[self.foreign.pk]), {}).status_code, 404)
|
|
|
|
def test_documents_block_foreign_pharmacy_before_webdav(self):
|
|
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
|
|
response = self.client.get(f'/pharmacies/{self.foreign.pk}/documents/')
|
|
self.assertEqual(response.status_code, 404)
|
|
dav.assert_not_called()
|
|
|
|
def test_onlyoffice_token_rechecks_assignment(self):
|
|
from apps.nextcloud_connector.views import _resolve_token_relpath
|
|
payload = {'kind': 'pharmacy', 'pharmacy_id': self.assigned.pk, 'uid': self.user.pk, 'name': 'test.docx'}
|
|
self.assertIsNotNone(_resolve_token_relpath(payload))
|
|
Pharmacy.objects.filter(pk=self.assigned.pk).update(curator=None)
|
|
self.assertIsNone(_resolve_token_relpath(payload))
|
|
|
|
def test_utilities_json_and_post_are_scoped(self):
|
|
from apps.utilities.models import UtilityMeterConfig, UtilityReading
|
|
from apps.utilities.views import _utilities_context
|
|
utility = self.directory('utility_type', 'scope-electricity')
|
|
for pharmacy in (self.own, self.foreign):
|
|
UtilityMeterConfig.objects.create(pharmacy=pharmacy, utility_type=utility)
|
|
context = _utilities_context(self.request())
|
|
self.assertNotIn(f'{self.foreign.pk}:', context['meter_configs_json'])
|
|
response = self.client.post('/accounting/?tab=utilities', {
|
|
'pharmacy': self.foreign.pk, 'utility_type': utility.pk,
|
|
'reading_date': '2026-09-21', 'value': '10',
|
|
})
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertFalse(UtilityReading.objects.exists())
|
|
|
|
def test_power_scope_covers_list_counts_and_relocation(self):
|
|
from apps.power_assets.models import PowerAsset
|
|
from apps.power_assets.views import _power_assets_context
|
|
kind, status = self.directory('power_asset_type', 'generator'), self.directory('power_asset_status', 'active')
|
|
own = PowerAsset.objects.create(location=self.own, asset_type=kind, status=status, model_name='Own')
|
|
other = PowerAsset.objects.create(location=self.foreign, asset_type=kind, status=status, model_name='Secret')
|
|
context = _power_assets_context(self.request())
|
|
self.assertEqual(context['power_counts']['total'], 1)
|
|
self.assertEqual(list(context['power_assets_page'].object_list), [own])
|
|
response = self.client.post(reverse('power_asset_update', args=[own.pk]), {
|
|
'location_id': self.foreign.pk, 'asset_type': kind.pk, 'model_name': 'Moved',
|
|
})
|
|
self.assertEqual(response.status_code, 404)
|
|
own.refresh_from_db()
|
|
self.assertEqual(own.location_id, self.own.pk)
|
|
self.assertEqual(self.client.post(reverse('power_asset_update', args=[other.pk]), {}).status_code, 404)
|
|
|
|
def test_it_foreign_reference_rejected_before_write(self):
|
|
from apps.it_manager.models import WorkLog
|
|
response = self.client.post(reverse('it_worklog_create'), {
|
|
'location_id': self.foreign.pk, 'description': 'Intrusion', 'work_type': 'other',
|
|
})
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertFalse(WorkLog.objects.exists())
|
|
|
|
def test_network_report_export_denied(self):
|
|
"""Экспорт живого отчёта — только руководителю отдела, не scoped pharmacy-user."""
|
|
with patch('apps.reports.views.build_monthly_report') as builder:
|
|
response = self.client.get('/reports/it/monthly/2026-09.docx')
|
|
self.assertEqual(response.status_code, 403)
|
|
builder.assert_not_called()
|
|
|
|
def test_department_snapshot_visible_foreign_hidden(self):
|
|
"""Опубликованный снимок своего отдела виден; чужого отдела — нет."""
|
|
from apps.reports.documents import user_can_view_report_document
|
|
from apps.reports.models import ReportDocument
|
|
dept = self.directory('department', 'scope-dept')
|
|
other = self.directory('department', 'other-dept')
|
|
self.user.department = dept
|
|
self.user.save(update_fields=['department'])
|
|
own = ReportDocument.objects.create(
|
|
department=dept, title='Own', filename='own.docx',
|
|
kind=ReportDocument.KIND_SNAPSHOT, status=ReportDocument.STATUS_PUBLISHED,
|
|
created_by=self.user,
|
|
)
|
|
foreign = ReportDocument.objects.create(
|
|
department=other, title='Foreign', filename='foreign.docx',
|
|
kind=ReportDocument.KIND_SNAPSHOT, status=ReportDocument.STATUS_PUBLISHED,
|
|
created_by=self.user,
|
|
)
|
|
self.assertTrue(user_can_view_report_document(self.user, own))
|
|
self.assertFalse(user_can_view_report_document(self.user, foreign))
|
|
|
|
def test_assignment_edit_requires_separate_permission(self):
|
|
from apps.locations.forms import PortalPharmacyEditForm
|
|
form = PortalPharmacyEditForm(instance=self.own, user=self.user)
|
|
for field in ('curator', 'responsible_manager', 'head_pharmacist'):
|
|
self.assertNotIn(field, form.fields)
|