Files
ACORE/apps/locations/tests.py
T
lokses ae5c1f1848 Add RRO, portal section perms, dislocation presets, and hiring intern flow.
Ship cash-register registry, section-based portal access, configurable dislocation columns, and trial-hire intern accounts with credentials PDF.
2026-08-09 14:58:17 +03:00

262 lines
12 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 unittest import mock
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, transaction
from django.test import TestCase
from core.models import Directory, DirectoryType
from apps.locations.models import Pharmacy
class CarbonioEmailTests(TestCase):
"""Генерация адреса Carbonio (<префикс организации><номер>@домен),
ручной override и проверка уникальности."""
@classmethod
def setUpTestData(cls):
org_type = DirectoryType.objects.create(code='organization', name='Организация')
region_type = DirectoryType.objects.create(code='region', name='Регион')
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
cls.aks = Directory.objects.create(
directory_type=org_type, code='akc', name='АКС', extra_data={'mail_prefix': 'a'})
cls.tira = Directory.objects.create(
directory_type=org_type, code='tira', name='Тира', extra_data={'mail_prefix': 't'})
cls.noprefix = Directory.objects.create(
directory_type=org_type, code='xx', name='Без префикса', extra_data={})
def _pharmacy(self, org, number, **kw):
return Pharmacy.objects.create(
organization=org, location_region=self.region,
pharmacy_number=number, location_type='pharmacy', **kw)
def test_build_email_format(self):
self.assertEqual(self._pharmacy(self.aks, 5).build_carbonio_email(), 'a5@aks-apteka.com.ua')
def test_build_email_uses_org_prefix(self):
self.assertEqual(self._pharmacy(self.tira, 5).build_carbonio_email(), 't5@aks-apteka.com.ua')
def test_build_email_empty_without_prefix(self):
self.assertEqual(self._pharmacy(self.noprefix, 5).build_carbonio_email(), '')
def test_autogenerate_on_save_when_enabled(self):
p = self._pharmacy(self.aks, 7, carbonio_sync_enabled=True)
self.assertEqual(p.carbonio_email, 'a7@aks-apteka.com.ua')
def test_no_autogenerate_when_disabled(self):
self.assertEqual(self._pharmacy(self.aks, 8).carbonio_email, '')
def test_manual_override_respected(self):
p = self._pharmacy(self.aks, 9, carbonio_sync_enabled=True,
carbonio_email='custom@aks-apteka.com.ua')
self.assertEqual(p.carbonio_email, 'custom@aks-apteka.com.ua')
def test_uniqueness_rejected_in_clean(self):
self._pharmacy(self.aks, 10, carbonio_sync_enabled=True) # a10@
p2 = self._pharmacy(self.tira, 11)
p2.carbonio_email = 'a10@aks-apteka.com.ua'
with self.assertRaises(ValidationError):
p2.clean()
def test_uniqueness_enforced_by_db(self):
self._pharmacy(self.aks, 12, carbonio_sync_enabled=True) # a12@
with self.assertRaises(IntegrityError):
with transaction.atomic():
self._pharmacy(self.tira, 13, carbonio_email='a12@aks-apteka.com.ua')
def test_autogenerate_skips_on_collision(self):
# ручной a15@ занят другой аптекой -> автоген для АКС №15 оставляет пусто
self._pharmacy(self.tira, 99, carbonio_sync_enabled=True,
carbonio_email='a15@aks-apteka.com.ua')
p = self._pharmacy(self.aks, 15, carbonio_sync_enabled=True)
self.assertEqual(p.carbonio_email, '')
def test_blank_emails_do_not_clash(self):
# несколько аптек без адреса — частичный уникальный индекс их не блокирует
self._pharmacy(self.aks, 16)
self._pharmacy(self.tira, 17)
self.assertEqual(Pharmacy.objects.filter(carbonio_email='').count(), 2)
class CarbonioPasswordSyncTests(TestCase):
"""sync_pharmacy_to_carbonio с замоканным CarbonioService: пароль ящика
сохраняется у нас зашифрованным и совпадает с отданным в Carbonio."""
@classmethod
def setUpTestData(cls):
org_type = DirectoryType.objects.create(code='organization', name='Организация')
region_type = DirectoryType.objects.create(code='region', name='Регион')
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
cls.aks = Directory.objects.create(
directory_type=org_type, code='akc', name='АКС', extra_data={'mail_prefix': 'a'})
def _pharmacy(self, number, **kw):
return Pharmacy.objects.create(
organization=self.aks, location_region=self.region,
pharmacy_number=number, location_type='pharmacy', **kw)
def test_create_stores_encrypted_password(self):
from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio
p = self._pharmacy(20) # синхронизация выключена, адрес пуст
fake = mock.Mock()
fake.get_account.return_value = None # аккаунта ещё нет → create
fake.create_account.return_value = 'acc-123'
with mock.patch(
'apps.mail_connector.services.sync_service.CarbonioService',
return_value=fake):
self.assertTrue(sync_pharmacy_to_carbonio(p))
p.refresh_from_db()
self.assertEqual(p.carbonio_email, 'a20@aks-apteka.com.ua')
self.assertEqual(p.carbonio_account_id, 'acc-123')
self.assertEqual(p.carbonio_sync_status, 'synced')
self.assertTrue(p.carbonio_password)
# тот же пароль ушёл в Carbonio, что и сохранён у нас
self.assertEqual(fake.create_account.call_args.args[1], p.carbonio_password)
# в БД пароль лежит зашифрованным
table, pk = Pharmacy._meta.db_table, Pharmacy._meta.pk.column
with connection.cursor() as cur:
cur.execute(f'SELECT carbonio_password FROM {table} WHERE {pk}=%s', [p.pk])
raw = cur.fetchone()[0]
self.assertTrue(raw.startswith('gAAAAA'))
def test_update_path_does_not_touch_password(self):
from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio
p = self._pharmacy(21, carbonio_email='a21@aks-apteka.com.ua')
fake = mock.Mock()
fake.get_account.return_value = {'id': 'acc-existing'} # аккаунт уже есть → update
with mock.patch(
'apps.mail_connector.services.sync_service.CarbonioService',
return_value=fake):
self.assertTrue(sync_pharmacy_to_carbonio(p))
p.refresh_from_db()
self.assertEqual(p.carbonio_account_id, 'acc-existing')
self.assertEqual(p.carbonio_password, '') # на update пароль не трогаем
fake.create_account.assert_not_called()
fake.set_account_status.assert_called_once_with('acc-existing', 'active')
class DislocationColumnsTests(TestCase):
"""Whitelist, резолв пресета default и сохранение preference пользователя."""
@classmethod
def setUpTestData(cls):
from datetime import date
from django.contrib.auth import get_user_model
from apps.locations.dislocation_columns import HR_COLUMNS, OPS_COLUMNS, STANDARD_COLUMNS
from apps.locations.models import DislocationColumnPreset
Employee = get_user_model()
org_type = DirectoryType.objects.create(code='organization', name='Организация')
region_type = DirectoryType.objects.create(code='region', name='Регион')
cls.region = Directory.objects.create(directory_type=region_type, code='kv', name='Киев')
cls.org = Directory.objects.create(directory_type=org_type, code='org', name='ТестОрг')
cls.user = Employee.objects.create(
username='dislo.cols',
email='dislo.cols@company.local',
first_name='Коля',
last_name='Колонки',
hire_date=date(2026, 1, 1),
user_location_type='office',
mattermost_sync_enabled=False,
ad_sync_enabled=False,
)
DislocationColumnPreset.objects.update_or_create(
code='standard',
defaults={
'name': 'Стандарт',
'columns': list(STANDARD_COLUMNS),
'is_default': True,
},
)
DislocationColumnPreset.objects.update_or_create(
code='hr',
defaults={'name': 'HR', 'columns': list(HR_COLUMNS), 'is_default': False},
)
DislocationColumnPreset.objects.update_or_create(
code='ops',
defaults={'name': 'Ops', 'columns': list(OPS_COLUMNS), 'is_default': False},
)
Pharmacy.objects.create(
organization=cls.org, location_region=cls.region,
pharmacy_number=101, location_type='pharmacy', street='Тестовая', house_number='1',
)
def test_normalize_whitelist_and_locked_pharmacy(self):
from apps.locations.dislocation_columns import normalize_columns
self.assertEqual(
normalize_columns(['email', 'pharmacy', 'unknown', 'email', 'region']),
['email', 'pharmacy', 'region'],
)
self.assertEqual(
normalize_columns(['organization', 'pharmacy', 'phone']),
['organization', 'pharmacy', 'phone'],
)
self.assertEqual(normalize_columns([]), ['pharmacy'])
self.assertEqual(normalize_columns(['secret', 'password']), ['pharmacy'])
# pharmacy обязательна, но если её нет в списке — добавляется первой
self.assertEqual(
normalize_columns(['region', 'phone']),
['pharmacy', 'region', 'phone'],
)
def test_resolve_uses_default_preset(self):
from apps.locations.dislocation_columns import STANDARD_COLUMNS, resolve_columns_for_user
self.user.dislocation_columns = None
self.user.save(update_fields=['dislocation_columns'])
self.assertEqual(resolve_columns_for_user(self.user), list(STANDARD_COLUMNS))
def test_resolve_uses_custom_preference(self):
from apps.locations.dislocation_columns import resolve_columns_for_user
self.user.dislocation_columns = ['region', 'phone', 'bogus']
self.user.save(update_fields=['dislocation_columns'])
self.assertEqual(
resolve_columns_for_user(self.user),
['pharmacy', 'region', 'phone'],
)
def test_post_saves_columns_preference(self):
self.client.force_login(self.user)
session = self.client.session
session['2fa_verified'] = True
session.save()
resp = self.client.post(
'/dislocation/columns/',
{'columns': ['organization', 'pharmacy', 'staff', 'region', 'email', 'evil']},
HTTP_HX_REQUEST='true',
)
self.assertEqual(resp.status_code, 200)
self.user.refresh_from_db()
self.assertEqual(
self.user.dislocation_columns,
['organization', 'pharmacy', 'staff', 'region', 'email'],
)
self.assertContains(resp, "disloSortBy('organization'")
self.assertContains(resp, "disloSortBy('region'")
self.assertContains(resp, "disloSortBy('email'")
self.assertNotContains(resp, "disloSortBy('phone'")
def test_post_preset_code_applies_hr(self):
from apps.locations.dislocation_columns import HR_COLUMNS
self.client.force_login(self.user)
session = self.client.session
session['2fa_verified'] = True
session.save()
resp = self.client.post(
'/dislocation/columns/',
{'preset_code': 'hr'},
HTTP_HX_REQUEST='true',
)
self.assertEqual(resp.status_code, 200)
self.user.refresh_from_db()
self.assertEqual(self.user.dislocation_columns, list(HR_COLUMNS))
self.assertContains(resp, "disloSortBy('curator'")
self.assertContains(resp, "disloSortBy('manager'")
self.assertNotContains(resp, "disloSortBy('phone'")