feat: expand platform APIs, sources, and test coverage
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped

This commit is contained in:
2026-03-17 12:56:48 +01:00
parent b505c67968
commit 3d298ce352
101 changed files with 8387 additions and 292 deletions

View File

@@ -0,0 +1,27 @@
"""Tests for exchange models."""
from apps.exchange.models import ExchangeConnection
from django.test import TestCase
from tests.apps.exchange.factories import ExchangeConnectionFactory
class ExchangeConnectionModelTest(TestCase):
def test_string_representation_and_plain_password_passthrough(self):
connection = ExchangeConnectionFactory(
username="postgres",
server="127.0.0.1",
port=5432,
database_name="target_db",
schema_name="public",
)
self.assertEqual(str(connection), "postgres@127.0.0.1:5432/target_db[public]")
self.assertEqual(ExchangeConnection.decrypt_password("legacy-pass"), "legacy-pass")
def test_decrypt_password_raises_for_invalid_encrypted_token(self):
with self.assertRaisesMessage(
ValueError,
"Не удалось расшифровать пароль exchange connection",
):
ExchangeConnection.decrypt_password(f"{ExchangeConnection.PASSWORD_PREFIX}invalid")

View File

@@ -0,0 +1,29 @@
"""Tests for exchange serializers."""
from apps.exchange.serializers import ExchangeCopyRequestSerializer
from django.test import SimpleTestCase
class ExchangeCopyRequestSerializerTest(SimpleTestCase):
def test_single_mode_requires_table(self):
serializer = ExchangeCopyRequestSerializer(data={"mode": "single"})
self.assertFalse(serializer.is_valid())
self.assertIn("table", serializer.errors)
def test_selected_mode_requires_tables(self):
serializer = ExchangeCopyRequestSerializer(data={"mode": "selected"})
self.assertFalse(serializer.is_valid())
self.assertIn("tables", serializer.errors)
def test_table_and_tables_are_rejected_for_wrong_modes(self):
serializer_with_table = ExchangeCopyRequestSerializer(
data={"mode": "all", "table": "parsers_manufacturer"}
)
self.assertFalse(serializer_with_table.is_valid())
self.assertIn("table", serializer_with_table.errors)
serializer_with_tables = ExchangeCopyRequestSerializer(
data={"mode": "all", "tables": ["parsers_manufacturer"]}
)
self.assertFalse(serializer_with_tables.is_valid())
self.assertIn("tables", serializer_with_tables.errors)

View File

@@ -0,0 +1,546 @@
from __future__ import annotations
from contextlib import suppress
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from apps.exchange.services import ExchangeConnectionService, ExchangeServiceError
from apps.parsers.models import IndustrialCertificateRecord, ManufacturerRecord, ParserLoadLog
from django.test import TestCase
from tests.apps.exchange.factories import ExchangeConnectionFactory
class _FakeModel:
_meta = SimpleNamespace(
app_label="tests",
model_name="fake_model",
db_table="fake_table",
local_fields=[
SimpleNamespace(attname="id", name="id", column="id"),
SimpleNamespace(attname="name", name="name", column="name"),
],
pk=SimpleNamespace(attname="id"),
)
objects = MagicMock()
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class _AnotherFakeModel:
_meta = SimpleNamespace(
app_label="tests",
model_name="another_model",
db_table="another_table",
local_fields=[SimpleNamespace(attname="id", name="id", column="id")],
pk=SimpleNamespace(attname="id"),
)
class ExchangeConnectionServiceUnitTest(TestCase):
def test_create_active_connection_and_prepare_updates_last_check_on_success(self):
with patch.object(
ExchangeConnectionService,
"test_connection",
return_value="target_alias",
) as test_connection_mock:
with patch.object(
ExchangeConnectionService,
"validate_target_structure",
) as validate_mock:
connection = ExchangeConnectionService.create_active_connection_and_prepare(
server="127.0.0.1",
port=5432,
username="postgres",
password="secret",
database_name="target_db",
schema_name="public",
)
self.assertTrue(connection.is_active)
self.assertIsNotNone(connection.last_checked_at)
self.assertEqual(connection.last_error, "")
test_connection_mock.assert_called_once_with(connection)
validate_mock.assert_called_once_with(
connection=connection,
alias="target_alias",
schema_name="public",
)
def test_get_active_connection_raises_when_missing(self):
with self.assertRaisesMessage(
ExchangeServiceError,
"Активное подключение не найдено",
):
ExchangeConnectionService.get_active_connection()
def test_test_connection_success_runs_select(self):
connection = ExchangeConnectionFactory()
db_connection = MagicMock()
cursor = MagicMock()
db_connection.cursor.return_value.__enter__.return_value = cursor
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="exchange_target_1",
):
with patch("apps.exchange.services.connections", connections_mock):
alias = ExchangeConnectionService.test_connection(connection)
self.assertEqual(alias, "exchange_target_1")
db_connection.ensure_connection.assert_called_once_with()
cursor.execute.assert_called_once_with("SELECT 1")
def test_test_connection_marks_error_on_failure(self):
connection = ExchangeConnectionFactory(last_error="")
db_connection = MagicMock()
db_connection.ensure_connection.side_effect = RuntimeError("boom")
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="exchange_target_1",
):
with patch("apps.exchange.services.connections", connections_mock):
with self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка подключения к целевой БД: boom",
):
ExchangeConnectionService.test_connection(connection)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "boom")
self.assertIsNotNone(connection.last_checked_at)
def test_validate_target_structure_calls_all_validation_steps(self):
connection = ExchangeConnectionFactory()
db_connection = MagicMock()
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel],
) as extend_mock:
with patch.object(
ExchangeConnectionService,
"_get_parser_models",
return_value=[_FakeModel],
):
with patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
) as schema_mock:
with patch.object(
ExchangeConnectionService,
"_validate_tables_exist",
) as tables_mock:
with patch.object(
ExchangeConnectionService,
"_validate_columns_exist",
) as columns_mock:
ExchangeConnectionService.validate_target_structure(
connection=connection,
alias="target_alias",
schema_name="public",
)
db_connection.ensure_connection.assert_called_once_with()
extend_mock.assert_called_once()
schema_mock.assert_called_once_with(alias="target_alias", schema_name="public")
tables_mock.assert_called_once_with(
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel],
)
columns_mock.assert_called_once_with(
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel],
)
def test_validate_target_structure_marks_and_reraises_exchange_error(self):
connection = ExchangeConnectionFactory(last_error="")
db_connection = MagicMock()
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
side_effect=ExchangeServiceError("bad schema"),
):
with self.assertRaisesMessage(ExchangeServiceError, "bad schema"):
ExchangeConnectionService.validate_target_structure(
connection=connection,
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel],
)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "bad schema")
def test_validate_target_structure_wraps_generic_error(self):
connection = ExchangeConnectionFactory(last_error="")
db_connection = MagicMock()
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
side_effect=RuntimeError("unexpected"),
):
with self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка проверки структуры целевой БД: unexpected",
):
ExchangeConnectionService.validate_target_structure(
connection=connection,
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel],
)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "unexpected")
def test_copy_parsers_data_success(self):
connection = ExchangeConnectionFactory(schema_name="target_schema")
db_connection = MagicMock()
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="target_alias",
):
with patch.object(
ExchangeConnectionService,
"_resolve_models",
return_value=[_FakeModel, _AnotherFakeModel],
):
with patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel, _AnotherFakeModel],
):
with patch.object(
ExchangeConnectionService,
"validate_target_structure",
) as validate_mock:
with patch.object(
ExchangeConnectionService,
"_copy_model_data",
side_effect=[2, 3],
) as copy_mock:
result = ExchangeConnectionService.copy_parsers_data(
connection=connection,
mode="selected",
tables=["fake_table", "another_table"],
truncate_before_copy=False,
)
self.assertEqual(result["mode"], "selected")
self.assertEqual(result["tables"], ["fake_table", "another_table"])
self.assertEqual(result["rows_by_table"], {"fake_table": 2, "another_table": 3})
self.assertEqual(result["total_rows"], 5)
self.assertFalse(result["truncate_before_copy"])
validate_mock.assert_called_once_with(
connection=connection,
alias="target_alias",
schema_name="target_schema",
models_to_copy=[_FakeModel, _AnotherFakeModel],
)
self.assertEqual(copy_mock.call_count, 2)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "")
self.assertIsNotNone(connection.last_checked_at)
def test_copy_parsers_data_marks_connection_error_on_connect_failure(self):
connection = ExchangeConnectionFactory(last_error="")
db_connection = MagicMock()
db_connection.ensure_connection.side_effect = RuntimeError("target unavailable")
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="target_alias",
):
with patch.object(
ExchangeConnectionService,
"_resolve_models",
return_value=[_FakeModel],
):
with patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel],
):
with self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка подключения к целевой БД: target unavailable",
):
ExchangeConnectionService.copy_parsers_data(
connection=connection,
mode="all",
)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "target unavailable")
def test_configure_alias_closes_existing_connection_and_clears_cache(self):
connection = ExchangeConnectionFactory(password="secret")
alias = f"exchange_target_{connection.id}"
existing_db_connection = MagicMock()
storage = SimpleNamespace(**{alias: "stale"})
connections_mock = MagicMock()
connections_mock.databases = {alias: {"ENGINE": "old"}}
connections_mock.__getitem__.return_value = existing_db_connection
connections_mock._connections = storage
with patch("apps.exchange.services.connections", connections_mock):
configured_alias = ExchangeConnectionService._configure_alias(connection)
self.assertEqual(configured_alias, alias)
existing_db_connection.close.assert_called_once_with()
self.assertEqual(connections_mock.databases[alias]["NAME"], connection.database_name)
self.assertEqual(connections_mock.databases[alias]["PASSWORD"], "secret")
self.assertNotIn(alias, storage.__dict__)
def test_validate_schema_exists_raises_when_schema_missing(self):
cursor = MagicMock()
cursor.fetchone.return_value = None
db_connection = MagicMock()
db_connection.cursor.return_value.__enter__.return_value = cursor
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with self.assertRaisesMessage(
ExchangeServiceError,
"Схема 'public' отсутствует в целевой БД",
):
ExchangeConnectionService._validate_schema_exists(
alias="target_alias",
schema_name="public",
)
def test_validate_tables_exist_raises_when_tables_missing(self):
cursor = MagicMock()
cursor.fetchall.return_value = [("fake_table",)]
db_connection = MagicMock()
db_connection.cursor.return_value.__enter__.return_value = cursor
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with self.assertRaisesMessage(
ExchangeServiceError,
"В целевой БД отсутствуют таблицы: another_table",
):
ExchangeConnectionService._validate_tables_exist(
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel, _AnotherFakeModel],
)
def test_validate_columns_exist_raises_when_columns_missing(self):
cursor_context = MagicMock()
cursor = MagicMock()
cursor.fetchall.return_value = [("id",)]
cursor_context.__enter__.return_value = cursor
db_connection = MagicMock()
db_connection.cursor.return_value = cursor_context
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
with self.assertRaisesMessage(
ExchangeServiceError,
"В таблице 'fake_table' отсутствуют колонки: name",
):
ExchangeConnectionService._validate_columns_exist(
alias="target_alias",
schema_name="public",
models_to_copy=[_FakeModel],
)
def test_get_parser_models_uses_configured_labels(self):
resolved_models = [_FakeModel for _ in ExchangeConnectionService.PARSER_MODEL_LABELS]
with patch(
"apps.exchange.services.django_apps.get_model",
side_effect=resolved_models,
) as get_model_mock:
result = ExchangeConnectionService._get_parser_models()
self.assertEqual(result, resolved_models)
self.assertEqual(
[call.args[0] for call in get_model_mock.call_args_list],
ExchangeConnectionService.PARSER_MODEL_LABELS,
)
def test_resolve_models_supports_table_and_class_names(self):
with patch.object(
ExchangeConnectionService,
"_get_parser_models",
return_value=[ParserLoadLog, ManufacturerRecord],
):
selected = ExchangeConnectionService._resolve_models(
mode="selected",
table=None,
tables=["parsers_load_log", "manufacturerrecord"],
)
self.assertEqual(selected, [ParserLoadLog, ManufacturerRecord])
def test_resolve_models_raises_on_unknown_table(self):
with patch.object(
ExchangeConnectionService,
"_get_parser_models",
return_value=[ParserLoadLog],
):
with self.assertRaisesMessage(ExchangeServiceError, "Неизвестная таблица"):
ExchangeConnectionService._resolve_models(
mode="single",
table="unknown_table",
tables=None,
)
def test_truncate_tables_executes_in_reverse_order(self):
cursor = MagicMock()
db_connection = MagicMock()
db_connection.cursor.return_value.__enter__.return_value = cursor
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch("apps.exchange.services.connections", connections_mock):
ExchangeConnectionService._truncate_tables(
alias="target_alias",
models_to_copy=[_FakeModel, _AnotherFakeModel],
)
executed_sql = [call.args[0] for call in cursor.execute.call_args_list]
self.assertEqual(
executed_sql,
[
'TRUNCATE TABLE "another_table" RESTART IDENTITY CASCADE',
'TRUNCATE TABLE "fake_table" RESTART IDENTITY CASCADE',
],
)
def test_copy_model_data_splits_batches(self):
source_objects = [
SimpleNamespace(id=1, name="A"),
SimpleNamespace(id=2, name="B"),
SimpleNamespace(id=3, name="C"),
]
queryset = MagicMock()
queryset.order_by.return_value = queryset
queryset.iterator.return_value = source_objects
default_manager = MagicMock()
default_manager.all.return_value = queryset
_FakeModel.objects = MagicMock()
_FakeModel.objects.using.return_value = default_manager
with patch.object(
ExchangeConnectionService,
"_insert_batch",
side_effect=[2, 1],
) as insert_mock:
total = ExchangeConnectionService._copy_model_data(
model=_FakeModel,
alias="target_alias",
truncate_before_copy=True,
chunk_size=2,
)
self.assertEqual(total, 3)
self.assertEqual(insert_mock.call_count, 2)
def test_insert_batch_returns_batch_size_in_truncate_mode(self):
manager = MagicMock()
_FakeModel.objects = MagicMock()
_FakeModel.objects.using.return_value = manager
batch = [_FakeModel(id=1, name="A"), _FakeModel(id=2, name="B")]
created_count = ExchangeConnectionService._insert_batch(
model=_FakeModel,
alias="target_alias",
batch=batch,
pk_name="id",
chunk_size=100,
truncate_before_copy=True,
)
self.assertEqual(created_count, 2)
manager.bulk_create.assert_called_once_with(
batch,
batch_size=100,
ignore_conflicts=False,
)
def test_insert_batch_counts_only_new_rows_without_truncate(self):
manager = MagicMock()
manager.filter.side_effect = [
MagicMock(values_list=MagicMock(return_value=[1])),
MagicMock(values_list=MagicMock(return_value=[1, 2])),
]
_FakeModel.objects = MagicMock()
_FakeModel.objects.using.return_value = manager
batch = [_FakeModel(id=1, name="A"), _FakeModel(id=2, name="B")]
created_count = ExchangeConnectionService._insert_batch(
model=_FakeModel,
alias="target_alias",
batch=batch,
pk_name="id",
chunk_size=100,
truncate_before_copy=False,
)
self.assertEqual(created_count, 1)
manager.bulk_create.assert_called_once_with(
batch,
batch_size=100,
ignore_conflicts=True,
)
def test_mark_connection_error_updates_connection(self):
connection = ExchangeConnectionFactory(last_error="")
ExchangeConnectionService._mark_connection_error(connection, "broken")
connection.refresh_from_db()
self.assertEqual(connection.last_error, "broken")
self.assertIsNotNone(connection.last_checked_at)
def tearDown(self):
for alias in list(getattr(ExchangeConnectionService, "__dict__", {})):
if alias.startswith("exchange_target_"):
with suppress(Exception):
from django.db import connections
connections[alias].close()
with suppress(Exception):
from django.db import connections
connections.databases.pop(alias, None)

View File

@@ -1,8 +1,12 @@
"""Tests for exchange services."""
from contextlib import suppress
from apps.exchange.models import ExchangeConnection
from apps.exchange.services import ExchangeConnectionService
from apps.parsers.models import IndustrialCertificateRecord, ParserLoadLog
from apps.registers.models import Organization
from django.db import connections
from django.test import TestCase
@@ -23,3 +27,63 @@ class ExchangeConnectionServiceDependenciesTest(TestCase):
self.assertEqual(models_to_copy[0], Organization)
self.assertEqual(models_to_copy[1], IndustrialCertificateRecord)
class ExchangeConnectionEncryptionTest(TestCase):
def test_save_encrypts_password(self):
connection = ExchangeConnection.objects.create(
server="127.0.0.1",
port=5432,
username="postgres",
password="secret", # noqa: S106
database_name="target_db",
schema_name="public",
)
self.assertNotEqual(connection.password, "secret")
self.assertTrue(ExchangeConnection.is_password_encrypted(connection.password))
self.assertEqual(connection.get_decrypted_password(), "secret")
def test_save_encrypts_legacy_password_on_partial_update(self):
connection = ExchangeConnection.objects.create(
server="127.0.0.1",
port=5432,
username="postgres",
password="secret", # noqa: S106
database_name="target_db",
schema_name="public",
)
ExchangeConnection.objects.filter(id=connection.id).update(
password="legacy-pass" # noqa: S106
)
connection.refresh_from_db()
self.assertEqual(connection.password, "legacy-pass")
connection.last_error = "checked"
connection.save(update_fields=["last_error", "updated_at"])
connection.refresh_from_db()
self.assertNotEqual(connection.password, "legacy-pass")
self.assertEqual(connection.get_decrypted_password(), "legacy-pass")
def test_configure_alias_uses_decrypted_password(self):
connection = ExchangeConnection.objects.create(
server="127.0.0.1",
port=5432,
username="postgres",
password="secret", # noqa: S106
database_name="target_db",
schema_name="public",
)
alias = ExchangeConnectionService._configure_alias(connection)
try:
self.assertEqual(connections.databases[alias]["PASSWORD"], "secret")
finally:
with suppress(Exception):
connections[alias].close()
connections.databases.pop(alias, None)
storage = getattr(connections, "_connections", None)
if storage is not None and hasattr(storage, "__dict__"):
storage.__dict__.pop(alias, None)

View File

@@ -0,0 +1,127 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
from apps.exchange.tasks import copy_parsers_data_async
from django.test import SimpleTestCase
class ExchangeTasksTest(SimpleTestCase):
def test_copy_parsers_data_async_completes_with_existing_job(self):
background_job = MagicMock()
connection = MagicMock()
copy_parsers_data_async.push_request(id="task-1")
try:
with patch(
"apps.exchange.tasks.BackgroundJobService.get_by_task_id_or_none",
return_value=background_job,
) as get_job_mock:
with patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock:
with patch(
"apps.exchange.tasks.ExchangeConnectionService.copy_parsers_data",
return_value={
"mode": "all",
"tables": ["fake_table"],
"rows_by_table": {"fake_table": 3},
"total_rows": 3,
"truncate_before_copy": True,
},
) as copy_mock:
filter_mock.return_value.first.return_value = connection
result = copy_parsers_data_async.run(
connection_id=11,
payload={"mode": "all", "truncate_before_copy": True},
requested_by_id=7,
)
finally:
copy_parsers_data_async.pop_request()
self.assertEqual(result["status"], "success")
self.assertEqual(result["connection_id"], 11)
get_job_mock.assert_called_once_with("task-1")
background_job.mark_started.assert_called_once_with()
background_job.update_progress.assert_any_call(
10,
"Проверка структуры целевой БД",
)
background_job.update_progress.assert_any_call(90, "Фиксация результата")
background_job.complete.assert_called_once_with(result=result)
copy_mock.assert_called_once_with(
connection=connection,
mode="all",
truncate_before_copy=True,
)
def test_copy_parsers_data_async_creates_job_and_fails_when_connection_missing(self):
background_job = MagicMock()
copy_parsers_data_async.push_request(id=None)
try:
with patch("apps.exchange.tasks.uuid.uuid4", return_value="generated-task-id"):
with patch(
"apps.exchange.tasks.BackgroundJobService.get_by_task_id_or_none",
return_value=None,
):
with patch(
"apps.exchange.tasks.BackgroundJobService.create_job",
return_value=background_job,
) as create_job_mock:
with patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock:
filter_mock.return_value.first.return_value = None
with self.assertRaisesMessage(
ValueError,
"Active exchange connection not found: 42",
):
copy_parsers_data_async.run(
connection_id=42,
payload={"mode": "all"},
requested_by_id=3,
)
finally:
copy_parsers_data_async.pop_request()
create_job_mock.assert_called_once_with(
task_id="generated-task-id",
task_name="apps.exchange.tasks.copy_parsers_data_async",
user_id=3,
meta={"connection_id": 42, "mode": "all"},
)
background_job.fail.assert_called_once_with(error="Активное подключение не найдено")
def test_copy_parsers_data_async_marks_failure_and_reraises(self):
background_job = MagicMock()
connection = MagicMock()
copy_parsers_data_async.push_request(id="task-2")
try:
with patch(
"apps.exchange.tasks.BackgroundJobService.get_by_task_id_or_none",
return_value=background_job,
):
with patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock:
with patch(
"apps.exchange.tasks.ExchangeConnectionService.copy_parsers_data",
side_effect=RuntimeError("copy failed"),
):
with patch("apps.exchange.tasks.logger.exception") as logger_mock:
filter_mock.return_value.first.return_value = connection
with self.assertRaisesMessage(RuntimeError, "copy failed"):
copy_parsers_data_async.run(
connection_id=9,
payload={"mode": "selected", "tables": ["fake_table"]},
)
finally:
copy_parsers_data_async.pop_request()
background_job.fail.assert_called_once_with(error="copy failed")
logger_mock.assert_called_once()

View File

@@ -56,6 +56,8 @@ class ExchangeViewsTest(APITestCase):
new_connection = ExchangeConnection.objects.get(id=response.data["data"]["id"])
self.assertTrue(new_connection.is_active)
self.assertNotEqual(new_connection.password, payload["password"])
self.assertEqual(new_connection.get_decrypted_password(), payload["password"])
old_active.refresh_from_db()
self.assertFalse(old_active.is_active)