Carbonio CE: хранение пароля ящика аптеки (шифрованно)

Вариант А — пароль ящика хранится обратимо-зашифрованным, чтобы показать
заведующему для входа:

- Pharmacy.carbonio_password (EncryptedCharField) — шифруется в БД
- sync_service при создании ящика генерирует пароль, отдаёт его в Carbonio
  и сохраняет свою зашифрованную копию; на update-пути пароль не трогается
- Пароль виден в админке аптеки (readonly, расшифрованный) — вкладка Carbonio
- 2 теста (мок CarbonioService): пароль сохранён, совпадает с отданным в
  Carbonio, в БД лежит шифртекстом; update не перетирает пароль

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 10:29:40 +03:00
parent 3be444b05e
commit 01f0c6f002
5 changed files with 100 additions and 10 deletions
+63 -1
View File
@@ -1,5 +1,7 @@
from unittest import mock
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db import IntegrityError, connection, transaction
from django.test import TestCase
from core.models import Directory, DirectoryType
@@ -73,3 +75,63 @@ class CarbonioEmailTests(TestCase):
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')