fix pre-commit
Some checks failed
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) Successful in 1m42s
CI/CD Pipeline / Run Tests (pull_request) Successful in 2m25s
CI/CD Pipeline / Telegram Notify Success (pull_request) Successful in 1m34s

This commit is contained in:
2026-03-17 13:55:34 +01:00
parent 3d298ce352
commit 25176f31b4
31 changed files with 653 additions and 553 deletions

View File

@@ -17,11 +17,15 @@ class ExchangeConnectionModelTest(TestCase):
)
self.assertEqual(str(connection), "postgres@127.0.0.1:5432/target_db[public]")
self.assertEqual(ExchangeConnection.decrypt_password("legacy-pass"), "legacy-pass")
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")
ExchangeConnection.decrypt_password(
f"{ExchangeConnection.PASSWORD_PREFIX}invalid"
)

View File

@@ -5,12 +5,19 @@ 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 apps.parsers.models import (
ManufacturerRecord,
ParserLoadLog,
)
from django.test import TestCase
from tests.apps.exchange.factories import ExchangeConnectionFactory
def _db_secret() -> str:
return "secret"
class _FakeModel:
_meta = SimpleNamespace(
app_label="tests",
@@ -45,19 +52,18 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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",
)
) as test_connection_mock, 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=_db_secret(),
database_name="target_db",
schema_name="public",
)
self.assertTrue(connection.is_active)
self.assertIsNotNone(connection.last_checked_at)
@@ -88,9 +94,8 @@ class ExchangeConnectionServiceUnitTest(TestCase):
ExchangeConnectionService,
"_configure_alias",
return_value="exchange_target_1",
):
with patch("apps.exchange.services.connections", connections_mock):
alias = ExchangeConnectionService.test_connection(connection)
), 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()
@@ -107,13 +112,13 @@ class ExchangeConnectionServiceUnitTest(TestCase):
ExchangeConnectionService,
"_configure_alias",
return_value="exchange_target_1",
), patch(
"apps.exchange.services.connections", connections_mock
), self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка подключения к целевой БД: boom",
):
with patch("apps.exchange.services.connections", connections_mock):
with self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка подключения к целевой БД: boom",
):
ExchangeConnectionService.test_connection(connection)
ExchangeConnectionService.test_connection(connection)
connection.refresh_from_db()
self.assertEqual(connection.last_error, "boom")
@@ -125,34 +130,31 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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",
)
with patch(
"apps.exchange.services.connections", connections_mock
), patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel],
) as extend_mock, patch.object(
ExchangeConnectionService,
"_get_parser_models",
return_value=[_FakeModel],
), patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
) as schema_mock, patch.object(
ExchangeConnectionService,
"_validate_tables_exist",
) as tables_mock, 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()
@@ -174,19 +176,19 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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],
)
with patch(
"apps.exchange.services.connections", connections_mock
), patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
side_effect=ExchangeServiceError("bad schema"),
), 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")
@@ -197,22 +199,22 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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],
)
with patch(
"apps.exchange.services.connections", connections_mock
), patch.object(
ExchangeConnectionService,
"_validate_schema_exists",
side_effect=RuntimeError("unexpected"),
), 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")
@@ -223,37 +225,34 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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,
)
with patch(
"apps.exchange.services.connections", connections_mock
), patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="target_alias",
), patch.object(
ExchangeConnectionService,
"_resolve_models",
return_value=[_FakeModel, _AnotherFakeModel],
), patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel, _AnotherFakeModel],
), patch.object(
ExchangeConnectionService,
"validate_target_structure",
) as validate_mock, 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"])
@@ -278,36 +277,34 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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",
)
with patch(
"apps.exchange.services.connections", connections_mock
), patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="target_alias",
), patch.object(
ExchangeConnectionService,
"_resolve_models",
return_value=[_FakeModel],
), patch.object(
ExchangeConnectionService,
"_extend_models_with_dependencies",
return_value=[_FakeModel],
), 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")
connection = ExchangeConnectionFactory(password=_db_secret())
alias = f"exchange_target_{connection.id}"
existing_db_connection = MagicMock()
storage = SimpleNamespace(**{alias: "stale"})
@@ -321,7 +318,9 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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]["NAME"], connection.database_name
)
self.assertEqual(connections_mock.databases[alias]["PASSWORD"], "secret")
self.assertNotIn(alias, storage.__dict__)
@@ -333,15 +332,16 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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",
)
with patch(
"apps.exchange.services.connections", connections_mock
), 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()
@@ -351,16 +351,17 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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],
)
with patch(
"apps.exchange.services.connections", connections_mock
), 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()
@@ -372,19 +373,22 @@ class ExchangeConnectionServiceUnitTest(TestCase):
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],
)
with patch(
"apps.exchange.services.connections", connections_mock
), 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]
resolved_models = [
_FakeModel for _ in ExchangeConnectionService.PARSER_MODEL_LABELS
]
with patch(
"apps.exchange.services.django_apps.get_model",
@@ -417,13 +421,12 @@ class ExchangeConnectionServiceUnitTest(TestCase):
ExchangeConnectionService,
"_get_parser_models",
return_value=[ParserLoadLog],
):
with self.assertRaisesMessage(ExchangeServiceError, "Неизвестная таблица"):
ExchangeConnectionService._resolve_models(
mode="single",
table="unknown_table",
tables=None,
)
), self.assertRaisesMessage(ExchangeServiceError, "Неизвестная таблица"):
ExchangeConnectionService._resolve_models(
mode="single",
table="unknown_table",
tables=None,
)
def test_truncate_tables_executes_in_reverse_order(self):
cursor = MagicMock()

View File

@@ -16,27 +16,25 @@ class ExchangeTasksTest(SimpleTestCase):
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
) as get_job_mock, patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock, 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,
)
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()
@@ -56,34 +54,33 @@ class ExchangeTasksTest(SimpleTestCase):
truncate_before_copy=True,
)
def test_copy_parsers_data_async_creates_job_and_fails_when_connection_missing(self):
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,
)
with patch(
"apps.exchange.tasks.uuid.uuid4", return_value="generated-task-id"
), patch(
"apps.exchange.tasks.BackgroundJobService.get_by_task_id_or_none",
return_value=None,
), patch(
"apps.exchange.tasks.BackgroundJobService.create_job",
return_value=background_job,
) as create_job_mock, patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock, self.assertRaisesMessage(
ValueError,
"Active exchange connection not found: 42",
):
filter_mock.return_value.first.return_value = None
copy_parsers_data_async.run(
connection_id=42,
payload={"mode": "all"},
requested_by_id=3,
)
finally:
copy_parsers_data_async.pop_request()
@@ -93,7 +90,9 @@ class ExchangeTasksTest(SimpleTestCase):
user_id=3,
meta={"connection_id": 42, "mode": "all"},
)
background_job.fail.assert_called_once_with(error="Активное подключение не найдено")
background_job.fail.assert_called_once_with(
error="Активное подключение не найдено"
)
def test_copy_parsers_data_async_marks_failure_and_reraises(self):
background_job = MagicMock()
@@ -104,22 +103,22 @@ class ExchangeTasksTest(SimpleTestCase):
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"]},
)
), patch(
"apps.exchange.tasks.ExchangeConnection.objects.filter",
) as filter_mock, patch(
"apps.exchange.tasks.ExchangeConnectionService.copy_parsers_data",
side_effect=RuntimeError("copy failed"),
), patch(
"apps.exchange.tasks.logger.exception"
) as logger_mock, self.assertRaisesMessage(RuntimeError, "copy failed"):
filter_mock.return_value.first.return_value = connection
copy_parsers_data_async.run(
connection_id=9,
payload={
"mode": "selected",
"tables": ["fake_table"],
},
)
finally:
copy_parsers_data_async.pop_request()