01f0c6f002
Вариант А — пароль ящика хранится обратимо-зашифрованным, чтобы показать заведующему для входа: - Pharmacy.carbonio_password (EncryptedCharField) — шифруется в БД - sync_service при создании ящика генерирует пароль, отдаёт его в Carbonio и сохраняет свою зашифрованную копию; на update-пути пароль не трогается - Пароль виден в админке аптеки (readonly, расшифрованный) — вкладка Carbonio - 2 теста (мок CarbonioService): пароль сохранён, совпадает с отданным в Carbonio, в БД лежит шифртекстом; update не перетирает пароль Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
138 lines
6.9 KiB
Python
138 lines
6.9 KiB
Python
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')
|