Files
ACORE/core/tests_location_access.py
lokses 9c8d0d109a mail: несколько ящиков с переключателем; дислокация без скоупа локаций
Сотрудник может подключить несколько IMAP-ящиков и переключаться между ними.
Дислокация снова показывает всю сеть, а не только назначенные аптеки.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 15:56:13 +03:00

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)