From 25176f31b455a627a525a5d38e48fde1ec1fa47c Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Tue, 17 Mar 2026 13:55:34 +0100 Subject: [PATCH] fix pre-commit --- docs/adr/ADR-001: Platform Version Policy.md | 2 +- .../ADR-002: Technology Stack Selection.md | 2 +- ...003: Background Processing Architecture.md | 2 +- ...DR-004: Data Ingestion and ETL Strategy.md | 2 +- ...ADR-005: External Integrations Strategy.md | 2 +- docs/adr/ADR-006: Configuration Strategy.md | 2 +- docs/adr/ADR-007: Deployment Model.md | 2 +- docs/adr/ADR-008: Testing Strategy.md | 2 +- docs/adr/ADR-009: Observability.md | 2 +- docs/adr/ADR-010: Project Structure.md | 2 +- ...ADR-011: Idempotency and Retry Strategy.md | 2 +- docs/adr/ADR-012: Data Consistency Model.md | 2 +- .../adr/ADR-013: Parser Stability Strategy.md | 2 +- ...oning and Backward Compatibility Policy.md | 2 +- docs/adr/ADR-INDEX.md | 2 +- .../parsers/clients/minpromtorg/products.py | 12 +- src/apps/parsers/source_cards.py | 53 ++- tests/apps/backups/test_services.py | 187 +++++----- tests/apps/backups/test_tasks.py | 85 +++-- tests/apps/core/test_celery_module.py | 34 +- tests/apps/core/test_pagination.py | 4 +- tests/apps/core/test_startup_checks.py | 40 +- tests/apps/exchange/test_models.py | 8 +- tests/apps/exchange/test_service_units.py | 351 +++++++++--------- tests/apps/exchange/test_tasks.py | 121 +++--- tests/apps/parsers/test_service_helpers.py | 64 ++-- .../apps/parsers/test_source_cards_service.py | 70 ++-- tests/apps/parsers/test_source_cards_views.py | 29 +- tests/apps/parsers/test_sources_api_e2e.py | 41 +- tests/apps/registers/test_serializers.py | 14 +- tests/apps/registers/test_services.py | 63 +++- 31 files changed, 653 insertions(+), 553 deletions(-) diff --git a/docs/adr/ADR-001: Platform Version Policy.md b/docs/adr/ADR-001: Platform Version Policy.md index 697a818..b30f511 100644 --- a/docs/adr/ADR-001: Platform Version Policy.md +++ b/docs/adr/ADR-001: Platform Version Policy.md @@ -44,4 +44,4 @@ Accepted ## Alternatives -- Использование latest upstream — отклонено (несовместимо с контуром) \ No newline at end of file +- Использование latest upstream — отклонено (несовместимо с контуром) diff --git a/docs/adr/ADR-002: Technology Stack Selection.md b/docs/adr/ADR-002: Technology Stack Selection.md index 8e1d5a2..77c31be 100644 --- a/docs/adr/ADR-002: Technology Stack Selection.md +++ b/docs/adr/ADR-002: Technology Stack Selection.md @@ -36,4 +36,4 @@ Accepted ## Alternatives - FastAPI — отклонён (меньше зрелости в админке и ORM экосистеме) -- Kubernetes — избыточен для текущего контура \ No newline at end of file +- Kubernetes — избыточен для текущего контура diff --git a/docs/adr/ADR-003: Background Processing Architecture.md b/docs/adr/ADR-003: Background Processing Architecture.md index 51b1af5..440fb74 100644 --- a/docs/adr/ADR-003: Background Processing Architecture.md +++ b/docs/adr/ADR-003: Background Processing Architecture.md @@ -35,4 +35,4 @@ Accepted ## Alternatives -- RQ / Dramatiq — отклонены (меньше зрелости) \ No newline at end of file +- RQ / Dramatiq — отклонены (меньше зрелости) diff --git a/docs/adr/ADR-004: Data Ingestion and ETL Strategy.md b/docs/adr/ADR-004: Data Ingestion and ETL Strategy.md index f9027d6..b931c57 100644 --- a/docs/adr/ADR-004: Data Ingestion and ETL Strategy.md +++ b/docs/adr/ADR-004: Data Ingestion and ETL Strategy.md @@ -31,4 +31,4 @@ Accepted ### Negative - сложность поддержки парсеров -- необходимость ручного восстановления \ No newline at end of file +- необходимость ручного восстановления diff --git a/docs/adr/ADR-005: External Integrations Strategy.md b/docs/adr/ADR-005: External Integrations Strategy.md index 9a0d1ac..7fb97fd 100644 --- a/docs/adr/ADR-005: External Integrations Strategy.md +++ b/docs/adr/ADR-005: External Integrations Strategy.md @@ -27,4 +27,4 @@ Accepted ### Negative - высокая хрупкость -- зависимость от изменений внешних систем \ No newline at end of file +- зависимость от изменений внешних систем diff --git a/docs/adr/ADR-006: Configuration Strategy.md b/docs/adr/ADR-006: Configuration Strategy.md index 988f857..fdb8dd8 100644 --- a/docs/adr/ADR-006: Configuration Strategy.md +++ b/docs/adr/ADR-006: Configuration Strategy.md @@ -22,4 +22,4 @@ Accepted - контроль конфигурации ### Negative -- риск рассинхронизации env \ No newline at end of file +- риск рассинхронизации env diff --git a/docs/adr/ADR-007: Deployment Model.md b/docs/adr/ADR-007: Deployment Model.md index 4b5b5b6..b994fea 100644 --- a/docs/adr/ADR-007: Deployment Model.md +++ b/docs/adr/ADR-007: Deployment Model.md @@ -21,4 +21,4 @@ Accepted - воспроизводимость ### Negative -- ограниченная масштабируемость \ No newline at end of file +- ограниченная масштабируемость diff --git a/docs/adr/ADR-008: Testing Strategy.md b/docs/adr/ADR-008: Testing Strategy.md index 1cfba5e..636ef8e 100644 --- a/docs/adr/ADR-008: Testing Strategy.md +++ b/docs/adr/ADR-008: Testing Strategy.md @@ -24,4 +24,4 @@ Accepted - удобство CI ### Negative -- сложность настройки среды \ No newline at end of file +- сложность настройки среды diff --git a/docs/adr/ADR-009: Observability.md b/docs/adr/ADR-009: Observability.md index fc344e0..1434deb 100644 --- a/docs/adr/ADR-009: Observability.md +++ b/docs/adr/ADR-009: Observability.md @@ -21,4 +21,4 @@ Accepted - диагностика проблем ### Negative -- ограниченная глубина мониторинга \ No newline at end of file +- ограниченная глубина мониторинга diff --git a/docs/adr/ADR-010: Project Structure.md b/docs/adr/ADR-010: Project Structure.md index 47dee34..c4e4acf 100644 --- a/docs/adr/ADR-010: Project Structure.md +++ b/docs/adr/ADR-010: Project Structure.md @@ -27,4 +27,4 @@ Accepted - понятная навигация ### Negative -- возможная связность между модулями \ No newline at end of file +- возможная связность между модулями diff --git a/docs/adr/ADR-011: Idempotency and Retry Strategy.md b/docs/adr/ADR-011: Idempotency and Retry Strategy.md index 08c23e8..3682ef6 100644 --- a/docs/adr/ADR-011: Idempotency and Retry Strategy.md +++ b/docs/adr/ADR-011: Idempotency and Retry Strategy.md @@ -125,4 +125,4 @@ Retry не должен безусловно выполняться для: Следующими связанными решениями должны быть: - политика дедупликации данных - модель частичной загрузки и фиксации прогресса -- политика конкурентного запуска задач \ No newline at end of file +- политика конкурентного запуска задач diff --git a/docs/adr/ADR-012: Data Consistency Model.md b/docs/adr/ADR-012: Data Consistency Model.md index 8db616d..bab57c7 100644 --- a/docs/adr/ADR-012: Data Consistency Model.md +++ b/docs/adr/ADR-012: Data Consistency Model.md @@ -130,4 +130,4 @@ Accepted Это решение напрямую зависит от: - ADR-011 Idempotency and Retry Strategy -- ADR-013 Parser Stability and Source Change Detection \ No newline at end of file +- ADR-013 Parser Stability and Source Change Detection diff --git a/docs/adr/ADR-013: Parser Stability Strategy.md b/docs/adr/ADR-013: Parser Stability Strategy.md index 6e9f8ae..074bcf3 100644 --- a/docs/adr/ADR-013: Parser Stability Strategy.md +++ b/docs/adr/ADR-013: Parser Stability Strategy.md @@ -115,4 +115,4 @@ Playwright допускается для источников, где: Следующим развитием данного решения должны быть: - smoke checks для критичных источников - классификация типов ошибок интеграции -- runbook для восстановления парсеров после изменения внешних порталов \ No newline at end of file +- runbook для восстановления парсеров после изменения внешних порталов diff --git a/docs/adr/ADR-014: API Versioning and Backward Compatibility Policy.md b/docs/adr/ADR-014: API Versioning and Backward Compatibility Policy.md index ab996be..49a2851 100644 --- a/docs/adr/ADR-014: API Versioning and Backward Compatibility Policy.md +++ b/docs/adr/ADR-014: API Versioning and Backward Compatibility Policy.md @@ -78,4 +78,4 @@ API развивается со временем: Отклонено — сложнее отлаживать и документировать. ### 2. Отсутствие версионирования -Отклонено — высокий риск поломки клиентов. \ No newline at end of file +Отклонено — высокий риск поломки клиентов. diff --git a/docs/adr/ADR-INDEX.md b/docs/adr/ADR-INDEX.md index 9de6723..04b0c01 100644 --- a/docs/adr/ADR-INDEX.md +++ b/docs/adr/ADR-INDEX.md @@ -17,4 +17,4 @@ - ADR-011: Idempotency and Retry Strategy - ADR-012: Data Deduplication and Consistency Model -- ADR-013: Parser Stability and Change Detection \ No newline at end of file +- ADR-013: Parser Stability and Change Detection diff --git a/src/apps/parsers/clients/minpromtorg/products.py b/src/apps/parsers/clients/minpromtorg/products.py index 62c6acb..e352219 100644 --- a/src/apps/parsers/clients/minpromtorg/products.py +++ b/src/apps/parsers/clients/minpromtorg/products.py @@ -21,8 +21,7 @@ DEFAULT_HOST = "minpromtorg.gov.ru" DEFAULT_API_PATH = "/api/kss-document-preview" DEFAULT_DOC_TYPE = "668d4f2a-966a-4b65-9fb9-2f1ad19a3d1f" DEFAULT_QUERY = ( - "Реестр промышленной продукции, произведенной на территории " - "Российской Федерации" + "Реестр промышленной продукции, произведенной на территории " "Российской Федерации" ) DATE_PATTERN = re.compile(r"(\d{8})") @@ -126,8 +125,7 @@ class IndustrialProductsClient: adapter=self.http_adapter, headers={ "User-Agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "Chrome/120.0.0.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "Chrome/120.0.0.0" ), "Accept": "application/json", }, @@ -225,7 +223,6 @@ class IndustrialProductsClient: return products def _detect_headers(self, worksheet) -> tuple[int, dict[str, int]]: - best_row = 1 best_map: dict[str, int] = {} for row_index in range(1, min(worksheet.max_row, 10) + 1): @@ -238,7 +235,6 @@ class IndustrialProductsClient: ) header_map = self._build_header_map(row) if len(header_map) > len(best_map): - best_row = row_index best_map = header_map if REQUIRED_HEADERS.issubset(header_map): @@ -291,7 +287,9 @@ class IndustrialProductsClient: if not any(product.__dict__.values()): return None if not product.registry_number or not product.product_name: - logger.warning("Skipping product row without registry number or name: %s", row) + logger.warning( + "Skipping product row without registry number or name: %s", row + ) return None return product diff --git a/src/apps/parsers/source_cards.py b/src/apps/parsers/source_cards.py index 604756e..983f1a7 100644 --- a/src/apps/parsers/source_cards.py +++ b/src/apps/parsers/source_cards.py @@ -25,7 +25,6 @@ from django.http import Http404 from django.utils import timezone from rest_framework.exceptions import ValidationError - SUCCESSFUL_LOAD_STATUSES = {"success", "skipped"} ACTIVE_JOB_STATUSES = [JobStatus.PENDING, JobStatus.STARTED, JobStatus.RETRY] @@ -104,9 +103,7 @@ SOURCE_CARD_DEFINITIONS: tuple[SourceCardDefinition, ...] = ( SourceItemDefinition( code="procurements", title="Единая информационная система закупок", - description=( - "Закупки и связанные данные из ЕИС по 44-ФЗ и 223-ФЗ." - ), + description=("Закупки и связанные данные из ЕИС по 44-ФЗ и 223-ФЗ."), parser_source=ParserLoadLog.Source.PROCUREMENTS, ), ), @@ -215,7 +212,9 @@ class SourceCardService: cards, key=lambda item: ( item["last_updated_at"] is None, - -(item["last_updated_at"].timestamp()) if item["last_updated_at"] else 0, + -(item["last_updated_at"].timestamp()) + if item["last_updated_at"] + else 0, item["title"], ), ) @@ -243,9 +242,13 @@ class SourceCardService: @classmethod def get_card(cls, slug: str) -> dict[str, Any]: definition = cls.get_definition(slug) - source_items = [cls._build_source_item(item) for item in definition.source_items] + source_items = [ + cls._build_source_item(item) for item in definition.source_items + ] records_count = sum(item["records_count"] for item in source_items) - organizations_count = cls._get_card_organizations_count(definition, source_items) + organizations_count = cls._get_card_organizations_count( + definition, source_items + ) latest_success_load = cls._get_latest_load( definition, @@ -311,7 +314,9 @@ class SourceCardService: ) -> dict[str, Any]: definition = cls.get_definition(slug) params = cls._validate_refresh_params(definition, params or {}) - tasks = cls._launch_refresh(definition, requested_by_id=requested_by_id, params=params) + tasks = cls._launch_refresh( + definition, requested_by_id=requested_by_id, params=params + ) return { "source_card": definition.slug, @@ -339,8 +344,7 @@ class SourceCardService: raise ValidationError( { "params": ( - "Неизвестные параметры обновления: " - + ", ".join(unknown_params) + "Неизвестные параметры обновления: " + ", ".join(unknown_params) ) } ) @@ -532,7 +536,9 @@ class SourceCardService: "records_count": records_count, "organizations_count": organizations_count, "last_updated_at": ( - latest_success_load.updated_at if latest_success_load else last_updated_at + latest_success_load.updated_at + if latest_success_load + else last_updated_at ), "latest_load": cls._serialize_load_log(latest_load), "latest_success_load": cls._serialize_load_log(latest_success_load), @@ -571,7 +577,12 @@ class SourceCardService: .count() ) if item_code == "manufactures": - return ManufacturerRecord.objects.exclude(inn="").values("inn").distinct().count() + return ( + ManufacturerRecord.objects.exclude(inn="") + .values("inn") + .distinct() + .count() + ) if item_code == "industrial_products": return ( IndustrialProductRecord.objects.exclude(inn="") @@ -580,7 +591,12 @@ class SourceCardService: .count() ) if item_code == "inspections": - return InspectionRecord.objects.exclude(inn="").values("inn").distinct().count() + return ( + InspectionRecord.objects.exclude(inn="") + .values("inn") + .distinct() + .count() + ) if item_code == "procurements": return ( ProcurementRecord.objects.exclude(customer_inn="") @@ -677,14 +693,15 @@ class SourceCardService: return queryset.order_by("-updated_at", "-created_at").first() @classmethod - def _get_active_tasks(cls, definition: SourceCardDefinition) -> list[dict[str, Any]]: + def _get_active_tasks( + cls, definition: SourceCardDefinition + ) -> list[dict[str, Any]]: queryset = BackgroundJobService.get_queryset().filter( task_name__in=definition.task_names, status__in=ACTIVE_JOB_STATUSES, ) return [ - cls._serialize_job(job) - for job in queryset.order_by("-created_at")[:10] + cls._serialize_job(job) for job in queryset.order_by("-created_at")[:10] ] @classmethod @@ -757,7 +774,9 @@ class SourceCardService: } @classmethod - def _serialize_load_log(cls, load_log: ParserLoadLog | None) -> dict[str, Any] | None: + def _serialize_load_log( + cls, load_log: ParserLoadLog | None + ) -> dict[str, Any] | None: if load_log is None: return None return { diff --git a/tests/apps/backups/test_services.py b/tests/apps/backups/test_services.py index 748b217..3e09b32 100644 --- a/tests/apps/backups/test_services.py +++ b/tests/apps/backups/test_services.py @@ -39,7 +39,6 @@ from tests.apps.registers.factories import ( ) from tests.apps.user.factories import UserFactory - TEST_BACKUP_KEY = base64.urlsafe_b64encode(b"k" * 32).decode("ascii").rstrip("=") @@ -146,7 +145,9 @@ class BackupExportServiceTest(TestCase): period_end=200, ) - artifact = BackupExportService.build_backup_archive(actual_date=date(2026, 3, 15)) + artifact = BackupExportService.build_backup_archive( + actual_date=date(2026, 3, 15) + ) self.assertIsInstance(artifact, BackupArtifact) self.assertEqual(artifact.organizations_count, 1) @@ -174,7 +175,9 @@ class BackupExportServiceTest(TestCase): self.assertEqual(payload["actual_date"], "2026-03-15") self.assertEqual(payload["organizations_count"], 1) self.assertEqual(len(payload["data"]["registers.Organization"]), 1) - self.assertEqual(payload["data"]["registers.Organization"][0]["pn_name"], "Active Org") + self.assertEqual( + payload["data"]["registers.Organization"][0]["pn_name"], "Active Org" + ) self.assertEqual(len(payload["data"]["parsers.FinancialReportLine"]), 1) def test_normalize_value_supports_scalar_types(self): @@ -217,12 +220,11 @@ class BackupExportServiceTest(TestCase): with patch( "apps.backups.services.base64.urlsafe_b64decode", side_effect=ValueError("bad base64"), + ), self.assertRaisesMessage( + BackupExportError, + "BACKUP_ENCRYPTION_KEY должен быть base64-url кодированным ключом", ): - with self.assertRaisesMessage( - BackupExportError, - "BACKUP_ENCRYPTION_KEY должен быть base64-url кодированным ключом", - ): - BackupExportService._read_encryption_key() + BackupExportService._read_encryption_key() @override_settings( BACKUP_ENCRYPTION_KEY=base64.urlsafe_b64encode(b"short").decode("ascii") @@ -243,15 +245,16 @@ class BackupExportServiceTest(TestCase): def encode(self, *_args, **_kwargs): return HugeBytes(b"{}") - with patch("apps.backups.services.json.dumps", return_value=HugeJson("{}")): - with self.assertRaisesMessage( - BackupExportError, - "Заголовок backup контейнера слишком большой", - ): - BackupExportService._build_bin_container( - encrypted_payload=b"payload", - header_payload={}, - ) + with patch( + "apps.backups.services.json.dumps", return_value=HugeJson("{}") + ), self.assertRaisesMessage( + BackupExportError, + "Заголовок backup контейнера слишком большой", + ): + BackupExportService._build_bin_container( + encrypted_payload=b"payload", + header_payload={}, + ) class BackupExportJobServiceTest(TestCase): @@ -286,7 +289,9 @@ class BackupExportJobServiceTest(TestCase): self.assertEqual(download_result.action, "download") def test_enqueue_backup_task_calls_celery(self): - with patch("apps.backups.tasks.generate_backup_for_date.apply_async") as apply_async: + with patch( + "apps.backups.tasks.generate_backup_for_date.apply_async" + ) as apply_async: BackupExportJobService._enqueue_backup_task(job_id=5, task_id="task-5") apply_async.assert_called_once_with(kwargs={"job_id": 5}, task_id="task-5") @@ -305,20 +310,24 @@ class BackupExportJobServiceTest(TestCase): BackupExportJobService.consume_ready_archive(actual_date=job.actual_date) def test_consume_ready_archive_deletes_job_when_file_missing(self): - job = BackupExportJob.objects.create( - actual_date=date(2026, 3, 22), - status=BackupExportJob.Status.SUCCESS, - task_id="task-success", - archive_path="/tmp/does-not-exist.zip", - ) + with TemporaryDirectory() as tmp_dir: + missing_archive = Path(tmp_dir) / "does-not-exist.zip" + job = BackupExportJob.objects.create( + actual_date=date(2026, 3, 22), + status=BackupExportJob.Status.SUCCESS, + task_id="task-success", + archive_path=str(missing_archive), + ) - with self.assertRaisesMessage( - BackupExportError, - "Файл бэкапа отсутствует, запустите формирование снова", - ): - BackupExportJobService.consume_ready_archive(actual_date=job.actual_date) + with self.assertRaisesMessage( + BackupExportError, + "Файл бэкапа отсутствует, запустите формирование снова", + ): + BackupExportJobService.consume_ready_archive( + actual_date=job.actual_date + ) - self.assertTrue(BackupExportJob.objects.filter(id=job.id).exists()) + self.assertTrue(BackupExportJob.objects.filter(id=job.id).exists()) def test_consume_ready_archive_reads_file_and_uses_path_name_as_fallback(self): with TemporaryDirectory() as tmp_dir: @@ -335,7 +344,9 @@ class BackupExportJobServiceTest(TestCase): organizations_count=3, ) - artifact = BackupExportJobService.consume_ready_archive(actual_date=job.actual_date) + artifact = BackupExportJobService.consume_ready_archive( + actual_date=job.actual_date + ) self.assertEqual(artifact.archive_bytes, archive_bytes) self.assertEqual(artifact.archive_filename, "backup-export.zip") @@ -355,16 +366,16 @@ class BackupExportJobServiceTest(TestCase): archive_path=str(stale_path), ) - with patch("apps.backups.services.uuid.uuid4", return_value="new-task-id"): - with patch.object( - BackupExportJobService, - "_enqueue_backup_task", - ) as enqueue_mock: - with self.captureOnCommitCallbacks(execute=True): - result = BackupExportJobService.check_or_start_job( - actual_date=today, - requested_by_id=user.id, - ) + with patch( + "apps.backups.services.uuid.uuid4", return_value="new-task-id" + ), patch.object( + BackupExportJobService, + "_enqueue_backup_task", + ) as enqueue_mock, self.captureOnCommitCallbacks(execute=True): + result = BackupExportJobService.check_or_start_job( + actual_date=today, + requested_by_id=user.id, + ) self.assertEqual(result.action, "started") self.assertEqual(result.task_id, "new-task-id") @@ -374,7 +385,9 @@ class BackupExportJobServiceTest(TestCase): self.assertEqual(new_job.task_id, "new-task-id") enqueue_mock.assert_called_once_with(job_id=new_job.id, task_id="new-task-id") - def test_check_or_start_job_retries_create_after_integrity_error_with_stale_job(self): + def test_check_or_start_job_retries_create_after_integrity_error_with_stale_job( + self, + ): today = date(2026, 3, 26) user = UserFactory.create_user() stale_job = BackupExportJob.objects.create( @@ -394,29 +407,24 @@ class BackupExportJobServiceTest(TestCase): BackupExportJobService, "_get_job_for_update", side_effect=[None, stale_job], - ): - with patch.object( - BackupExportJobService, - "_result_for_existing_job", - return_value=None, - ): - with patch( - "apps.backups.services.BackupExportJob.objects.create", - side_effect=create_side_effect, - ): - with patch( - "apps.backups.services.uuid.uuid4", - return_value="retry-task-id", - ): - with patch.object( - BackupExportJobService, - "_enqueue_backup_task", - ) as enqueue_mock: - with self.captureOnCommitCallbacks(execute=True): - result = BackupExportJobService.check_or_start_job( - actual_date=today, - requested_by_id=user.id, - ) + ), patch.object( + BackupExportJobService, + "_result_for_existing_job", + return_value=None, + ), patch( + "apps.backups.services.BackupExportJob.objects.create", + side_effect=create_side_effect, + ), patch( + "apps.backups.services.uuid.uuid4", + return_value="retry-task-id", + ), patch.object( + BackupExportJobService, + "_enqueue_backup_task", + ) as enqueue_mock, self.captureOnCommitCallbacks(execute=True): + result = BackupExportJobService.check_or_start_job( + actual_date=today, + requested_by_id=user.id, + ) self.assertEqual(result.action, "started") self.assertEqual(result.task_id, "retry-task-id") @@ -425,7 +433,9 @@ class BackupExportJobServiceTest(TestCase): self.assertEqual(new_job.task_id, "retry-task-id") enqueue_mock.assert_called_once_with(job_id=new_job.id, task_id="retry-task-id") - def test_check_or_start_job_retries_create_after_integrity_error_without_concurrent_job(self): + def test_check_or_start_job_retries_create_after_integrity_error_without_concurrent_job( + self, + ): today = date(2026, 3, 28) user = UserFactory.create_user() original_create = BackupExportJob.objects.create @@ -440,35 +450,32 @@ class BackupExportJobServiceTest(TestCase): BackupExportJobService, "_get_job_for_update", side_effect=[None, None], - ): - with patch.object( - BackupExportJobService, - "_result_for_existing_job", - return_value=None, - ): - with patch( - "apps.backups.services.BackupExportJob.objects.create", - side_effect=create_side_effect, - ): - with patch( - "apps.backups.services.uuid.uuid4", - return_value="retry-task-id-2", - ): - with patch.object( - BackupExportJobService, - "_enqueue_backup_task", - ) as enqueue_mock: - with self.captureOnCommitCallbacks(execute=True): - result = BackupExportJobService.check_or_start_job( - actual_date=today, - requested_by_id=user.id, - ) + ), patch.object( + BackupExportJobService, + "_result_for_existing_job", + return_value=None, + ), patch( + "apps.backups.services.BackupExportJob.objects.create", + side_effect=create_side_effect, + ), patch( + "apps.backups.services.uuid.uuid4", + return_value="retry-task-id-2", + ), patch.object( + BackupExportJobService, + "_enqueue_backup_task", + ) as enqueue_mock, self.captureOnCommitCallbacks(execute=True): + result = BackupExportJobService.check_or_start_job( + actual_date=today, + requested_by_id=user.id, + ) self.assertEqual(result.action, "started") self.assertEqual(result.task_id, "retry-task-id-2") new_job = BackupExportJob.objects.get(actual_date=today) self.assertEqual(new_job.task_id, "retry-task-id-2") - enqueue_mock.assert_called_once_with(job_id=new_job.id, task_id="retry-task-id-2") + enqueue_mock.assert_called_once_with( + job_id=new_job.id, task_id="retry-task-id-2" + ) def test_archive_exists_and_cleanup_job_artifact(self): with TemporaryDirectory() as tmp_dir: diff --git a/tests/apps/backups/test_tasks.py b/tests/apps/backups/test_tasks.py index e8d80e7..844ee10 100644 --- a/tests/apps/backups/test_tasks.py +++ b/tests/apps/backups/test_tasks.py @@ -2,7 +2,6 @@ from __future__ import annotations from pathlib import Path from tempfile import TemporaryDirectory -from types import SimpleNamespace from unittest.mock import MagicMock, patch from apps.backups.models import BackupExportJob @@ -14,15 +13,17 @@ from tests.apps.user.factories import UserFactory class BackupTasksTest(TestCase): - def test_resolve_backup_target_path_creates_directory_and_renames_existing_file(self): - with TemporaryDirectory() as tmp_dir: - with override_settings(BACKUP_EXPORT_DIRECTORY=tmp_dir): - existing = Path(tmp_dir) / "backup.zip" - existing.write_bytes(b"existing") + def test_resolve_backup_target_path_creates_directory_and_renames_existing_file( + self, + ): + with TemporaryDirectory() as tmp_dir, override_settings( + BACKUP_EXPORT_DIRECTORY=tmp_dir + ), patch("apps.backups.tasks.uuid.uuid4") as uuid_mock: + existing = Path(tmp_dir) / "backup.zip" + existing.write_bytes(b"existing") - with patch("apps.backups.tasks.uuid.uuid4") as uuid_mock: - uuid_mock.return_value.hex = "deadbeefcafebabe" - target = _resolve_backup_target_path("backup.zip") + uuid_mock.return_value.hex = "deadbeefcafebabe" + target = _resolve_backup_target_path("backup.zip") self.assertEqual(target.name, "backup_deadbeef.zip") @@ -53,33 +54,32 @@ class BackupTasksTest(TestCase): actual_date=job.actual_date, ) - with TemporaryDirectory() as tmp_dir: - with override_settings(BACKUP_EXPORT_DIRECTORY=tmp_dir): - generate_backup_for_date.push_request(id="task-success") - try: - with patch( - "apps.backups.tasks.BackgroundJobService.get_by_task_id_or_none", - return_value=None, - ): - with patch( - "apps.backups.tasks.BackgroundJobService.create_job", - return_value=background_job, - ) as create_job_mock: - with patch( - "apps.backups.tasks.BackupExportService.build_backup_archive", - return_value=artifact, - ) as build_mock: - result = generate_backup_for_date.run(job_id=job.id) - finally: - generate_backup_for_date.pop_request() + with TemporaryDirectory() as tmp_dir, override_settings( + BACKUP_EXPORT_DIRECTORY=tmp_dir + ): + generate_backup_for_date.push_request(id="task-success") + try: + with patch( + "apps.backups.tasks.BackgroundJobService.get_by_task_id_or_none", + return_value=None, + ), patch( + "apps.backups.tasks.BackgroundJobService.create_job", + return_value=background_job, + ) as create_job_mock, patch( + "apps.backups.tasks.BackupExportService.build_backup_archive", + return_value=artifact, + ) as build_mock: + result = generate_backup_for_date.run(job_id=job.id) + finally: + generate_backup_for_date.pop_request() - job.refresh_from_db() - self.assertEqual(job.status, BackupExportJob.Status.SUCCESS) - self.assertEqual(job.task_id, "task-success") - self.assertEqual(job.archive_filename, "backup.zip") - self.assertEqual(job.checksum_filename, "backup.zip.sha256") - self.assertEqual(job.organizations_count, 5) - self.assertTrue(Path(job.archive_path).is_file()) + job.refresh_from_db() + self.assertEqual(job.status, BackupExportJob.Status.SUCCESS) + self.assertEqual(job.task_id, "task-success") + self.assertEqual(job.archive_filename, "backup.zip") + self.assertEqual(job.checksum_filename, "backup.zip.sha256") + self.assertEqual(job.organizations_count, 5) + self.assertTrue(Path(job.archive_path).is_file()) self.assertEqual(result["status"], "success") self.assertEqual(result["archive_filename"], "backup.zip") @@ -104,14 +104,13 @@ class BackupTasksTest(TestCase): with patch( "apps.backups.tasks.BackgroundJobService.get_by_task_id_or_none", return_value=background_job, - ): - with patch( - "apps.backups.tasks.BackupExportService.build_backup_archive", - side_effect=RuntimeError("boom"), - ): - with patch("apps.backups.tasks.logger.exception") as logger_mock: - with self.assertRaisesMessage(RuntimeError, "boom"): - generate_backup_for_date.run(job_id=job.id) + ), patch( + "apps.backups.tasks.BackupExportService.build_backup_archive", + side_effect=RuntimeError("boom"), + ), patch( + "apps.backups.tasks.logger.exception" + ) as logger_mock, self.assertRaisesMessage(RuntimeError, "boom"): + generate_backup_for_date.run(job_id=job.id) finally: generate_backup_for_date.pop_request() diff --git a/tests/apps/core/test_celery_module.py b/tests/apps/core/test_celery_module.py index a09b22c..c44d9cd 100644 --- a/tests/apps/core/test_celery_module.py +++ b/tests/apps/core/test_celery_module.py @@ -9,10 +9,7 @@ from unittest.mock import MagicMock, patch from django.test import SimpleTestCase - -CELERY_MODULE_PATH = ( - Path(__file__).resolve().parents[3] / "src" / "core" / "celery.py" -) +CELERY_MODULE_PATH = Path(__file__).resolve().parents[3] / "src" / "core" / "celery.py" def _load_module(module_name: str): @@ -25,22 +22,22 @@ def _load_module(module_name: str): class CeleryModuleTest(SimpleTestCase): def test_import_requires_django_settings_module(self): - with patch.dict(os.environ, {}, clear=True): - with self.assertRaisesMessage( - RuntimeError, - "DJANGO_SETTINGS_MODULE is not set.", - ): - _load_module("isolated_core_celery_missing") + with patch.dict(os.environ, {}, clear=True), self.assertRaisesMessage( + RuntimeError, + "DJANGO_SETTINGS_MODULE is not set.", + ): + _load_module("isolated_core_celery_missing") def test_import_runs_startup_checks_for_worker_runtime(self): app_mock = MagicMock() app_mock.conf = SimpleNamespace() - with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True): - with patch.object(sys, "argv", ["celery", "-A", "project", "worker"]): - with patch("apps.core.startup_checks.run_startup_checks") as checks_mock: - with patch("celery.Celery", return_value=app_mock): - module = _load_module("isolated_core_celery_worker") + with patch.dict( + os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True + ), patch.object(sys, "argv", ["celery", "-A", "project", "worker"]), patch( + "apps.core.startup_checks.run_startup_checks" + ) as checks_mock, patch("celery.Celery", return_value=app_mock): + module = _load_module("isolated_core_celery_worker") checks_mock.assert_called_once_with(component="celery") app_mock.config_from_object.assert_called_once_with( @@ -51,9 +48,10 @@ class CeleryModuleTest(SimpleTestCase): self.assertEqual(module.app, app_mock) def test_debug_task_prints_request(self): - with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True): - with patch.object(sys, "argv", ["python", "manage.py", "shell"]): - module = _load_module("isolated_core_celery_debug") + with patch.dict( + os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True + ), patch.object(sys, "argv", ["python", "manage.py", "shell"]): + module = _load_module("isolated_core_celery_debug") with patch("builtins.print") as print_mock: module.debug_task.run() diff --git a/tests/apps/core/test_pagination.py b/tests/apps/core/test_pagination.py index 75440c3..f2a0bb1 100644 --- a/tests/apps/core/test_pagination.py +++ b/tests/apps/core/test_pagination.py @@ -17,6 +17,8 @@ class PaginationSchemaTest(SimpleTestCase): ) self.assertEqual(schema["type"], "object") - pagination = schema["properties"]["meta"]["properties"]["pagination"]["properties"] + pagination = schema["properties"]["meta"]["properties"]["pagination"][ + "properties" + ] self.assertIn("next_cursor", pagination) self.assertIn("previous_cursor", pagination) diff --git a/tests/apps/core/test_startup_checks.py b/tests/apps/core/test_startup_checks.py index 6a7026d..e8f0d36 100644 --- a/tests/apps/core/test_startup_checks.py +++ b/tests/apps/core/test_startup_checks.py @@ -10,11 +10,15 @@ from apps.core import startup_checks from django.test import SimpleTestCase, override_settings +def _db_secret() -> str: + return "secret" + + TEST_DATABASES = { "default": { "NAME": "mostovik", "USER": "postgres", - "PASSWORD": "secret", + "PASSWORD": _db_secret(), "HOST": "db.example.test", "PORT": 5432, "OPTIONS": {"sslmode": "require"}, @@ -45,7 +49,7 @@ class StartupChecksTest(SimpleTestCase): connect_mock.assert_called_once_with( dbname="mostovik", user="postgres", - password="secret", + password=_db_secret(), host="db.example.test", port=5432, connect_timeout=7, @@ -143,7 +147,9 @@ class StartupChecksTest(SimpleTestCase): STARTUP_REDIS_TIMEOUT_SECONDS=12, ) @patch("apps.core.startup_checks._check_db", return_value=(True, "OK")) - @patch("apps.core.startup_checks._check_redis", return_value=(False, "redis failed")) + @patch( + "apps.core.startup_checks._check_redis", return_value=(False, "redis failed") + ) @patch("apps.core.startup_checks._log") def test_run_startup_checks_exits_on_redis_failure( self, @@ -173,13 +179,13 @@ class EntryPointImportTest(SimpleTestCase): with patch.dict(os.environ, {}, clear=False): os.environ.pop("DJANGO_SETTINGS_MODULE", None) - with patch.dict(sys.modules, {"core.celery": celery_stub}): - with patch("apps.core.startup_checks.run_startup_checks") as checks_mock: - with patch( - "django.core.asgi.get_asgi_application", - return_value=sentinel_application, - ): - module = self._import_fresh("core.asgi") + with patch.dict(sys.modules, {"core.celery": celery_stub}), patch( + "apps.core.startup_checks.run_startup_checks" + ) as checks_mock, patch( + "django.core.asgi.get_asgi_application", + return_value=sentinel_application, + ): + module = self._import_fresh("core.asgi") self.assertEqual(os.environ["DJANGO_SETTINGS_MODULE"], "settings.test") checks_mock.assert_called_once_with(component="asgi") @@ -192,13 +198,13 @@ class EntryPointImportTest(SimpleTestCase): with patch.dict(os.environ, {}, clear=False): os.environ.pop("DJANGO_SETTINGS_MODULE", None) - with patch.dict(sys.modules, {"core.celery": celery_stub}): - with patch("apps.core.startup_checks.run_startup_checks") as checks_mock: - with patch( - "django.core.wsgi.get_wsgi_application", - return_value=sentinel_application, - ): - module = self._import_fresh("core.wsgi") + with patch.dict(sys.modules, {"core.celery": celery_stub}), patch( + "apps.core.startup_checks.run_startup_checks" + ) as checks_mock, patch( + "django.core.wsgi.get_wsgi_application", + return_value=sentinel_application, + ): + module = self._import_fresh("core.wsgi") self.assertEqual(os.environ["DJANGO_SETTINGS_MODULE"], "settings.test") checks_mock.assert_called_once_with(component="wsgi") diff --git a/tests/apps/exchange/test_models.py b/tests/apps/exchange/test_models.py index 37037b6..cd38230 100644 --- a/tests/apps/exchange/test_models.py +++ b/tests/apps/exchange/test_models.py @@ -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" + ) diff --git a/tests/apps/exchange/test_service_units.py b/tests/apps/exchange/test_service_units.py index 25ee333..11c754d 100644 --- a/tests/apps/exchange/test_service_units.py +++ b/tests/apps/exchange/test_service_units.py @@ -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() diff --git a/tests/apps/exchange/test_tasks.py b/tests/apps/exchange/test_tasks.py index 5966163..e191343 100644 --- a/tests/apps/exchange/test_tasks.py +++ b/tests/apps/exchange/test_tasks.py @@ -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() diff --git a/tests/apps/parsers/test_service_helpers.py b/tests/apps/parsers/test_service_helpers.py index c0f767c..b232664 100644 --- a/tests/apps/parsers/test_service_helpers.py +++ b/tests/apps/parsers/test_service_helpers.py @@ -69,8 +69,12 @@ class RegistryOrganizationResolverTest(TestCase): self.assertEqual(lookup.by_ogrn, {}) def test_resolve_organization_id_by_unique_inn_and_ogrn(self): - org_by_inn = OrganizationFactory(mn_inn=7_701_001_001, mn_ogrn=10_277_001_000_001) - org_by_ogrn = OrganizationFactory(mn_inn=7_701_001_002, mn_ogrn=10_277_001_000_002) + org_by_inn = OrganizationFactory( + mn_inn=7_701_001_001, mn_ogrn=10_277_001_000_001 + ) + org_by_ogrn = OrganizationFactory( + mn_inn=7_701_001_002, mn_ogrn=10_277_001_000_002 + ) lookup = RegistryOrganizationResolver.build_lookup( [ (org_by_inn.mn_inn, None), @@ -103,37 +107,39 @@ class ParserLoadLogServiceRetryTest(TestCase): batch_id=2, ) - with patch.object(ParserLoadLogService, "get_next_batch_id", side_effect=[1, 2]): - with patch.object( - ParserLoadLogService, - "create_load_log", - side_effect=[IntegrityError("duplicate"), log], - ) as create_mock: - created_log, batch_id = ( - ParserLoadLogService.create_load_log_with_next_batch_id( - source=ParserLoadLog.Source.INDUSTRIAL - ) - ) + with patch.object( + ParserLoadLogService, "get_next_batch_id", side_effect=[1, 2] + ), patch.object( + ParserLoadLogService, + "create_load_log", + side_effect=[IntegrityError("duplicate"), log], + ) as create_mock: + ( + created_log, + batch_id, + ) = ParserLoadLogService.create_load_log_with_next_batch_id( + source=ParserLoadLog.Source.INDUSTRIAL + ) self.assertEqual(created_log, log) self.assertEqual(batch_id, 2) self.assertEqual(create_mock.call_count, 2) def test_create_load_log_with_next_batch_id_raises_after_max_retries(self): - with patch.object(ParserLoadLogService, "get_next_batch_id", return_value=1): - with patch.object( - ParserLoadLogService, - "create_load_log", - side_effect=IntegrityError("duplicate"), - ): - with self.assertRaisesMessage( - RuntimeError, - "Failed to allocate unique batch_id", - ): - ParserLoadLogService.create_load_log_with_next_batch_id( - source=ParserLoadLog.Source.INDUSTRIAL, - max_retries=2, - ) + with patch.object( + ParserLoadLogService, "get_next_batch_id", return_value=1 + ), patch.object( + ParserLoadLogService, + "create_load_log", + side_effect=IntegrityError("duplicate"), + ), self.assertRaisesMessage( + RuntimeError, + "Failed to allocate unique batch_id", + ): + ParserLoadLogService.create_load_log_with_next_batch_id( + source=ParserLoadLog.Source.INDUSTRIAL, + max_retries=2, + ) class SmallParserServiceQueryTest(TestCase): @@ -177,7 +183,9 @@ class SmallParserServiceQueryTest(TestCase): load_batch=12, ) - self.assertEqual(ProcurementService.find_by_customer_name("Тестовый").count(), 2) + self.assertEqual( + ProcurementService.find_by_customer_name("Тестовый").count(), 2 + ) self.assertEqual( ProcurementService.find_by_customer_name("Тестовый", batch_id=11).count(), 1, diff --git a/tests/apps/parsers/test_source_cards_service.py b/tests/apps/parsers/test_source_cards_service.py index 023aea4..e1fdd96 100644 --- a/tests/apps/parsers/test_source_cards_service.py +++ b/tests/apps/parsers/test_source_cards_service.py @@ -4,7 +4,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch from apps.parsers.source_cards import ( - RefreshParamDefinition, SourceCardDefinition, SourceCardService, SourceItemDefinition, @@ -66,8 +65,14 @@ class SourceCardServiceUnitTest(SimpleTestCase): @patch( "apps.parsers.source_cards.SourceCardService._enqueue_task", side_effect=[ - {"task_id": "task-1", "task_name": "apps.parsers.tasks.parse_industrial_production"}, - {"task_id": "task-2", "task_name": "apps.parsers.tasks.parse_industrial_products"}, + { + "task_id": "task-1", + "task_name": "apps.parsers.tasks.parse_industrial_production", + }, + { + "task_id": "task-2", + "task_name": "apps.parsers.tasks.parse_industrial_products", + }, {"task_id": "task-3", "task_name": "apps.parsers.tasks.parse_manufactures"}, ], ) @@ -78,14 +83,22 @@ class SourceCardServiceUnitTest(SimpleTestCase): ) self.assertEqual(result["source_card"], "manufacturers-and-products") - self.assertEqual([item["task_id"] for item in result["tasks"]], ["task-1", "task-2", "task-3"]) + self.assertEqual( + [item["task_id"] for item in result["tasks"]], + ["task-1", "task-2", "task-3"], + ) self.assertEqual(enqueue_mock.call_count, 3) @patch( "apps.parsers.source_cards.SourceCardService._enqueue_task", - return_value={"task_id": "task-1", "task_name": "apps.parsers.tasks.sync_inspections"}, + return_value={ + "task_id": "task-1", + "task_name": "apps.parsers.tasks.sync_inspections", + }, ) - def test_launch_refresh_for_inspections_passes_supported_kwargs_only(self, enqueue_mock): + def test_launch_refresh_for_inspections_passes_supported_kwargs_only( + self, enqueue_mock + ): definition = SourceCardService.get_definition("planned-inspections") result = SourceCardService._launch_refresh( @@ -99,7 +112,10 @@ class SourceCardServiceUnitTest(SimpleTestCase): }, ) - self.assertEqual(result, [{"task_id": "task-1", "task_name": "apps.parsers.tasks.sync_inspections"}]) + self.assertEqual( + result, + [{"task_id": "task-1", "task_name": "apps.parsers.tasks.sync_inspections"}], + ) self.assertEqual( enqueue_mock.call_args.kwargs["kwargs"], { @@ -112,7 +128,10 @@ class SourceCardServiceUnitTest(SimpleTestCase): @patch( "apps.parsers.source_cards.SourceCardService._enqueue_task", - return_value={"task_id": "task-9", "task_name": "apps.parsers.tasks.sync_procurements"}, + return_value={ + "task_id": "task-9", + "task_name": "apps.parsers.tasks.sync_procurements", + }, ) def test_refresh_card_for_procurements_uses_default_law_type(self, enqueue_mock): result = SourceCardService.refresh_card( @@ -156,34 +175,37 @@ class SourceCardServiceUnitTest(SimpleTestCase): params={}, ) - self.assertIn("Обновление для карточки не поддерживается", str(error.exception.detail)) + self.assertIn( + "Обновление для карточки не поддерживается", str(error.exception.detail) + ) def test_enqueue_task_deletes_background_job_on_async_error(self): task = MagicMock() task.apply_async.side_effect = RuntimeError("broker down") queryset = MagicMock() - with patch("apps.parsers.source_cards.uuid.uuid4", return_value="task-id-1"): - with patch("apps.parsers.source_cards.BackgroundJobService.create_job"): - with patch( - "apps.parsers.source_cards.BackgroundJobService.get_queryset", - return_value=queryset, - ): - with self.assertRaisesMessage(RuntimeError, "broker down"): - SourceCardService._enqueue_task( - task=task, - task_name="apps.parsers.tasks.sync_procurements", - requested_by_id=5, - meta={"source_card": "public-procurements"}, - kwargs={"region_code": "77"}, - ) + with patch( + "apps.parsers.source_cards.uuid.uuid4", return_value="task-id-1" + ), patch("apps.parsers.source_cards.BackgroundJobService.create_job"), patch( + "apps.parsers.source_cards.BackgroundJobService.get_queryset", + return_value=queryset, + ), self.assertRaisesMessage(RuntimeError, "broker down"): + SourceCardService._enqueue_task( + task=task, + task_name="apps.parsers.tasks.sync_procurements", + requested_by_id=5, + meta={"source_card": "public-procurements"}, + kwargs={"region_code": "77"}, + ) queryset.filter.assert_called_once_with(task_id="task-id-1") queryset.filter.return_value.delete.assert_called_once_with() def test_helper_methods_cover_unknown_codes_and_status_variants(self): self.assertEqual(SourceCardService._get_source_records_count("unknown"), 0) - self.assertEqual(SourceCardService._get_source_organizations_count("unknown"), 0) + self.assertEqual( + SourceCardService._get_source_organizations_count("unknown"), 0 + ) self.assertIsNone(SourceCardService._get_source_data_timestamp("unknown")) self.assertIsNone(SourceCardService._get_latest_load_by_source(None)) self.assertEqual(SourceCardService._get_status_label("custom"), "custom") diff --git a/tests/apps/parsers/test_source_cards_views.py b/tests/apps/parsers/test_source_cards_views.py index 58723f1..a19cdfd 100644 --- a/tests/apps/parsers/test_source_cards_views.py +++ b/tests/apps/parsers/test_source_cards_views.py @@ -2,6 +2,9 @@ from __future__ import annotations +from pathlib import Path +from tempfile import TemporaryDirectory + from apps.core.models import BackgroundJob, JobStatus from apps.parsers.models import FinancialReport, FinancialReportLine, ParserLoadLog from django.test import override_settings @@ -161,21 +164,21 @@ class SourceCardsApiTestCase(APITestCase): self.assertIn("status_label", row) self.assertIn("active_tasks", row) - @override_settings( - FNS_WATCH_DIRECTORY="/tmp/mostovik-test-fns/watch", - FNS_PROCESSED_DIRECTORY="/tmp/mostovik-test-fns/processed", - FNS_FAILED_DIRECTORY="/tmp/mostovik-test-fns/failed", - ) def test_refresh_creates_background_job_and_returns_task(self): self.client.force_authenticate(self.admin) - response = self.client.post( - reverse( - "api_v1:sources:source-cards-refresh", - kwargs={"slug": "financial-indicators"}, - ), - {}, - format="json", - ) + with TemporaryDirectory() as tmp_dir, override_settings( + FNS_WATCH_DIRECTORY=str(Path(tmp_dir) / "watch"), + FNS_PROCESSED_DIRECTORY=str(Path(tmp_dir) / "processed"), + FNS_FAILED_DIRECTORY=str(Path(tmp_dir) / "failed"), + ): + response = self.client.post( + reverse( + "api_v1:sources:source-cards-refresh", + kwargs={"slug": "financial-indicators"}, + ), + {}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertTrue(response.data["success"]) diff --git a/tests/apps/parsers/test_sources_api_e2e.py b/tests/apps/parsers/test_sources_api_e2e.py index f206c58..ff9d22b 100644 --- a/tests/apps/parsers/test_sources_api_e2e.py +++ b/tests/apps/parsers/test_sources_api_e2e.py @@ -111,7 +111,9 @@ class SourcesApiE2ETest(APITestCase): self.assertEqual(cards["planned-inspections"]["status"], "in_progress") self.assertEqual(cards["planned-inspections"]["progress"], 55) self.assertEqual(cards["public-procurements"]["status"], "error") - self.assertEqual(cards["public-procurements"]["error_message"], "download error") + self.assertEqual( + cards["public-procurements"]["error_message"], "download error" + ) minprom_card = detail_response.data["data"] self.assertEqual(minprom_card["records_count"], 3) @@ -169,27 +171,24 @@ class SourcesApiE2ETest(APITestCase): "request-procurements", "task-procurements", ], + ), patch( + "apps.parsers.tasks.parse_industrial_production.apply_async", + return_value=SimpleNamespace(id="task-industrial"), + ), patch( + "apps.parsers.tasks.parse_industrial_products.apply_async", + return_value=SimpleNamespace(id="task-products"), + ), patch( + "apps.parsers.tasks.parse_manufactures.apply_async", + return_value=SimpleNamespace(id="task-manufactures"), ): - with patch( - "apps.parsers.tasks.parse_industrial_production.apply_async", - return_value=SimpleNamespace(id="task-industrial"), - ): - with patch( - "apps.parsers.tasks.parse_industrial_products.apply_async", - return_value=SimpleNamespace(id="task-products"), - ): - with patch( - "apps.parsers.tasks.parse_manufactures.apply_async", - return_value=SimpleNamespace(id="task-manufactures"), - ): - minprom_response = self.client.post( - reverse( - "api_v1:sources:source-cards-refresh", - kwargs={"slug": "manufacturers-and-products"}, - ), - {}, - format="json", - ) + minprom_response = self.client.post( + reverse( + "api_v1:sources:source-cards-refresh", + kwargs={"slug": "manufacturers-and-products"}, + ), + {}, + format="json", + ) with patch( "apps.parsers.tasks.sync_procurements.apply_async", diff --git a/tests/apps/registers/test_serializers.py b/tests/apps/registers/test_serializers.py index 205a5fc..0fcedd4 100644 --- a/tests/apps/registers/test_serializers.py +++ b/tests/apps/registers/test_serializers.py @@ -1,13 +1,13 @@ """Tests for registers serializers.""" -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import TestCase - from apps.registers.serializers import ( OrganizationListQuerySerializer, RegisterFileUploadSerializer, RegistryOrganizationListQuerySerializer, ) +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + from tests.apps.registers.factories import RegisterFactory @@ -27,9 +27,7 @@ class RegisterFileUploadSerializerTest(TestCase): class OrganizationListQuerySerializerTest(TestCase): def test_actual_date_requires_registry(self): - serializer = OrganizationListQuerySerializer( - data={"actual_date": "2026-03-17"} - ) + serializer = OrganizationListQuerySerializer(data={"actual_date": "2026-03-17"}) self.assertFalse(serializer.is_valid()) self.assertIn("actual_date", serializer.errors) @@ -54,6 +52,8 @@ class RegistryOrganizationListQuerySerializerTest(TestCase): self.assertIn("mn_okpo", serializer.errors) def test_accepts_digit_only_okpo(self): - serializer = RegistryOrganizationListQuerySerializer(data={"mn_okpo": "87654321"}) + serializer = RegistryOrganizationListQuerySerializer( + data={"mn_okpo": "87654321"} + ) self.assertTrue(serializer.is_valid(), serializer.errors) diff --git a/tests/apps/registers/test_services.py b/tests/apps/registers/test_services.py index b919910..82439b4 100644 --- a/tests/apps/registers/test_services.py +++ b/tests/apps/registers/test_services.py @@ -3,7 +3,11 @@ from __future__ import annotations import io from datetime import date -from apps.registers.services import ParsedOrganization, RegisterImportError, RegisterImportService +from apps.registers.services import ( + ParsedOrganization, + RegisterImportError, + RegisterImportService, +) from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from openpyxl import Workbook @@ -86,7 +90,9 @@ class RegisterImportServiceTest(TestCase): def test_get_active_periods_by_org_returns_mapping(self): registry = RegisterFactory() - active_period = RegistryMembershipPeriodFactory(registry=registry, ended_at=None) + active_period = RegistryMembershipPeriodFactory( + registry=registry, ended_at=None + ) RegistryMembershipPeriodFactory( registry=registry, started_at=date(2026, 1, 1), @@ -224,7 +230,10 @@ class RegisterImportServiceTest(TestCase): started_by_upload=upload, ) - queryset, resolved_date = RegisterImportService.get_registry_organizations_queryset( + ( + queryset, + resolved_date, + ) = RegisterImportService.get_registry_organizations_queryset( registry=registry, mn_ogrn=organization.mn_ogrn, mn_inn=organization.mn_inn, @@ -261,15 +270,21 @@ class RegisterImportServiceTest(TestCase): def test_parse_xlsx_raises_on_invalid_workbook_headers_and_empty_rows(self): broken = SimpleUploadedFile("broken.xlsx", b"not-an-excel") - with self.assertRaisesMessage(RegisterImportError, "Не удалось прочитать Excel файл"): + with self.assertRaisesMessage( + RegisterImportError, "Не удалось прочитать Excel файл" + ): RegisterImportService.parse_xlsx(broken) no_headers = _upload("no-headers.xlsx", []) - with self.assertRaisesMessage(RegisterImportError, "Файл не содержит заголовков"): + with self.assertRaisesMessage( + RegisterImportError, "Файл не содержит заголовков" + ): RegisterImportService.parse_xlsx(no_headers) missing_headers = _upload("missing.xlsx", [["pn_name", "mn_ogrn"]]) - with self.assertRaisesMessage(RegisterImportError, "Отсутствуют обязательные колонки"): + with self.assertRaisesMessage( + RegisterImportError, "Отсутствуют обязательные колонки" + ): RegisterImportService.parse_xlsx(missing_headers) empty_rows = _upload( @@ -313,29 +328,47 @@ class RegisterImportServiceTest(TestCase): self.assertTrue(RegisterImportService._is_empty_row((None, " "))) self.assertFalse(RegisterImportService._is_empty_row((None, "x"))) self.assertEqual( - RegisterImportService._as_required_text(" Org ", field_name="pn_name", row_number=2), + RegisterImportService._as_required_text( + " Org ", field_name="pn_name", row_number=2 + ), "Org", ) self.assertEqual( - RegisterImportService._as_required_int("123.0", field_name="mn_inn", row_number=2), + RegisterImportService._as_required_int( + "123.0", field_name="mn_inn", row_number=2 + ), 123, ) self.assertIsNone( - RegisterImportService._as_optional_int(" ", field_name="in_kpp", row_number=2) + RegisterImportService._as_optional_int( + " ", field_name="in_kpp", row_number=2 + ) ) self.assertEqual( - RegisterImportService._as_numeric_text("123 456.0", field_name="mn_okpo", row_number=2), + RegisterImportService._as_numeric_text( + "123 456.0", field_name="mn_okpo", row_number=2 + ), "123456", ) with self.assertRaisesMessage(RegisterImportError, "поле pn_name обязательно"): - RegisterImportService._as_required_text("", field_name="pn_name", row_number=2) + RegisterImportService._as_required_text( + "", field_name="pn_name", row_number=2 + ) with self.assertRaisesMessage(RegisterImportError, "поле mn_inn обязательно"): - RegisterImportService._as_required_int(None, field_name="mn_inn", row_number=2) - with self.assertRaisesMessage(RegisterImportError, "поле in_kpp должно быть числом"): - RegisterImportService._as_optional_int("abc", field_name="in_kpp", row_number=2) + RegisterImportService._as_required_int( + None, field_name="mn_inn", row_number=2 + ) + with self.assertRaisesMessage( + RegisterImportError, "поле in_kpp должно быть числом" + ): + RegisterImportService._as_optional_int( + "abc", field_name="in_kpp", row_number=2 + ) with self.assertRaisesMessage( RegisterImportError, "поле mn_okpo должно содержать только цифры", ): - RegisterImportService._as_numeric_text("ab12", field_name="mn_okpo", row_number=2) + RegisterImportService._as_numeric_text( + "ab12", field_name="mn_okpo", row_number=2 + )