feat: export state corp package from backup endpoint
This commit is contained in:
@@ -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"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user