feat: export state corp package from backup endpoint
Some checks failed
CI/CD Pipeline / Quality Gate (push) Successful in 33s
CI/CD Pipeline / Build and Push Images (push) Successful in 10s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Failing after 9s

This commit is contained in:
2026-05-12 15:12:56 +02:00
parent 15360a3c8e
commit 75c1d4cf1a
11 changed files with 925 additions and 301 deletions

View File

@@ -1,21 +1,13 @@
"""Tests for async backups export API."""
"""Tests for backups export API."""
from __future__ import annotations
import hashlib
from pathlib import Path
from tempfile import TemporaryDirectory
from datetime import date
from types import SimpleNamespace
from unittest.mock import patch
from apps.backups.models import BackupExportJob
from apps.backups.services import (
BackupExportError,
BackupExportJobService,
BackupRequestResult,
)
from django.db import IntegrityError
from apps.exchange.state_corp_services import StateCorpExchangeError
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
@@ -23,7 +15,7 @@ from tests.apps.user.factories import UserFactory
class BackupExportViewTest(APITestCase):
"""Tests for async backup export endpoint."""
"""Tests for synchronous ZIP export endpoint."""
def setUp(self):
self.user = UserFactory.create_user()
@@ -38,167 +30,49 @@ class BackupExportViewTest(APITestCase):
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_starts_job_when_absent(self, enqueue_mock):
self.client.force_authenticate(self.admin)
today = timezone.localdate()
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "started")
job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(job.status, BackupExportJob.Status.PENDING)
self.assertEqual(response.data["task_id"], job.task_id)
enqueue_mock.assert_called_once_with(job_id=job.id, task_id=job.task_id)
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_returns_wait_when_job_in_progress(self, enqueue_mock):
today = timezone.localdate()
BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.STARTED,
task_id="task-running-1",
@patch("apps.backups.views.StateCorpExchangeService.build_package")
def test_export_returns_state_corp_zip_synchronously(self, build_package_mock):
build_package_mock.return_value = SimpleNamespace(
package_id="package-1",
archive_name="state_corp_exchange.zip",
bin_name="state_corp_exchange.bin",
archive_bytes=b"zip-bytes",
payload_counts={
"organizations": 2,
"industrial_products": 1,
"prosecutor_checks": 0,
"public_procurements": 0,
"arbitration_cases": 0,
},
produced_at="2026-05-12T10:00:00+00:00",
)
self.client.force_authenticate(self.admin)
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
{"actual_date": "2026-05-12", "registry": "ignored-legacy-field"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "wait")
self.assertEqual(response.data["task_id"], "task-running-1")
enqueue_mock.assert_not_called()
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_returns_file_and_deletes_after_download(self, enqueue_mock):
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
archive_bytes = b"zip-content"
archive_path = tmp_path / "backup.zip"
archive_path.write_bytes(archive_bytes)
today = timezone.localdate()
job = BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.SUCCESS,
task_id="task-success-1",
archive_path=str(archive_path),
archive_filename="backup.zip",
checksum_filename="backup.zip.sha256",
checksum_sha256=hashlib.sha256(archive_bytes).hexdigest(),
organizations_count=7,
)
self.client.force_authenticate(self.admin)
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, archive_bytes)
self.assertEqual(response["Content-Type"], "application/zip")
self.assertIn(
'attachment; filename="backup.zip"', response["Content-Disposition"]
)
self.assertEqual(response["X-Backup-Organizations"], "7")
self.assertEqual(
response["X-Backup-SHA256"],
hashlib.sha256(archive_bytes).hexdigest(),
)
self.assertFalse(archive_path.exists())
self.assertFalse(BackupExportJob.objects.filter(id=job.id).exists())
enqueue_mock.assert_not_called()
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_restarts_when_success_job_has_no_file(self, enqueue_mock):
today = timezone.localdate()
with TemporaryDirectory() as tmp_dir:
missing_archive_path = Path(tmp_dir) / "non-existent-backup.zip"
BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.SUCCESS,
task_id="task-old",
archive_path=str(missing_archive_path),
archive_filename="non-existent-backup.zip",
)
self.client.force_authenticate(self.admin)
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "started")
restarted_job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(response.data["task_id"], restarted_job.task_id)
enqueue_mock.assert_called_once_with(
job_id=restarted_job.id,
task_id=restarted_job.task_id,
)
@patch("apps.backups.services.BackupExportJob.objects.create")
def test_check_or_start_job_handles_integrity_race(self, create_mock):
today = timezone.localdate()
concurrent_job = BackupExportJob(
actual_date=today,
status=BackupExportJob.Status.STARTED,
task_id="task-race-running",
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, b"zip-bytes")
self.assertEqual(response["Content-Type"], "application/zip")
self.assertIn(
'attachment; filename="state_corp_exchange.zip"',
response["Content-Disposition"],
)
concurrent_job.save()
create_mock.side_effect = IntegrityError("duplicate key value")
self.assertEqual(response["Content-Length"], "9")
self.assertEqual(response["X-State-Corp-Package-Id"], "package-1")
self.assertEqual(response["X-State-Corp-Bin-File"], "state_corp_exchange.bin")
self.assertEqual(response["X-State-Corp-Organizations"], "2")
self.assertEqual(response["X-Backup-Organizations"], "2")
self.assertEqual(response["X-Backup-Actual-Date"], "2026-05-12")
build_package_mock.assert_called_once_with(actual_date=date(2026, 5, 12))
with patch.object(
BackupExportJobService,
"_get_job_for_update",
side_effect=[None, concurrent_job],
):
result = BackupExportJobService.check_or_start_job(
actual_date=today,
requested_by_id=self.admin.id,
)
self.assertEqual(result.action, "wait")
self.assertEqual(result.task_id, "task-race-running")
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_check_or_start_job_enqueues_on_commit(self, enqueue_mock):
today = timezone.localdate()
with self.captureOnCommitCallbacks(execute=False) as callbacks:
result = BackupExportJobService.check_or_start_job(
actual_date=today,
requested_by_id=self.admin.id,
)
self.assertEqual(result.action, "started")
self.assertEqual(len(callbacks), 1)
enqueue_mock.assert_not_called()
job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(job.task_id, result.task_id)
callbacks[0]()
enqueue_mock.assert_called_once_with(job_id=job.id, task_id=job.task_id)
@patch("apps.backups.views.BackupExportJobService.check_or_start_job")
def test_export_returns_400_when_job_start_fails(self, check_or_start_mock):
@patch("apps.backups.views.StateCorpExchangeService.build_package")
def test_export_returns_400_when_package_build_fails(self, build_package_mock):
self.client.force_authenticate(self.admin)
check_or_start_mock.side_effect = BackupExportError("broken")
build_package_mock.side_effect = StateCorpExchangeError("broken")
response = self.client.post(self.url, {}, format="json")
@@ -206,32 +80,3 @@ class BackupExportViewTest(APITestCase):
self.assertEqual(
response.data["errors"][0]["details"]["fields"]["backup"], ["broken"]
)
@patch("apps.backups.views.BackupExportJobService.consume_ready_archive")
@patch("apps.backups.views.BackupExportJobService.check_or_start_job")
def test_export_returns_400_when_archive_consumption_fails(
self,
check_or_start_mock,
consume_mock,
):
today = timezone.localdate()
self.client.force_authenticate(self.admin)
check_or_start_mock.return_value = BackupRequestResult(
action="download",
message="ready",
actual_date=today,
task_id="task-ready",
)
consume_mock.side_effect = BackupExportError("missing archive")
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["errors"][0]["details"]["fields"]["backup"],
["missing archive"],
)