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"],
)

View File

@@ -12,6 +12,8 @@ from unittest.mock import Mock, patch
from zipfile import ZipFile
from apps.exchange.state_corp_services import StateCorpExchangeService
from apps.parsers.models import GenericParserRecord, ParserLoadLog
from apps.registers.models import Register
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from django.test import TestCase, override_settings
@@ -20,7 +22,11 @@ from tests.apps.parsers.factories import (
InspectionRecordFactory,
ProcurementRecordFactory,
)
from tests.apps.registers.factories import OrganizationFactory
from tests.apps.registers.factories import (
OrganizationFactory,
RegisterFactory,
RegistryMembershipPeriodFactory,
)
def _b64url_decode(value: str) -> bytes:
@@ -28,6 +34,25 @@ def _b64url_decode(value: str) -> bytes:
return base64.urlsafe_b64decode(normalized.encode("ascii"))
def _decode_package_payload(package):
with ZipFile(BytesIO(package.archive_bytes)) as archive:
bin_names = [name for name in archive.namelist() if name.endswith(".bin")]
assert len(bin_names) == 1
bin_bytes = archive.read(bin_names[0])
header_size = struct.unpack(">I", bin_bytes[5:9])[0]
header_end = 9 + header_size
header = json.loads(bin_bytes[9:header_end].decode("utf-8"))
encrypted_payload = bin_bytes[header_end:]
raw_key = hashlib.sha256(TEST_STATE_CORP_TOKEN.encode("utf-8")).digest()
compressed_payload = AESGCM(raw_key).decrypt(
_b64url_decode(header["nonce"]),
encrypted_payload,
_b64url_decode(header["aad"]),
)
return json.loads(zlib.decompress(compressed_payload).decode("utf-8"))
TEST_STATE_CORP_TOKEN = "state-corp-test-exchange-token" # noqa: S105
@@ -39,6 +64,7 @@ class StateCorpExchangeServiceTest(TestCase):
"""Verify package compatibility with state-corp receiver contract."""
def test_build_package_contains_expected_payload(self):
registry = Register.objects.get(name="Реестр госкорпорации Росатом")
organization = OrganizationFactory.create(
mn_inn=7707083893,
mn_ogrn=1027700132195,
@@ -46,6 +72,12 @@ class StateCorpExchangeServiceTest(TestCase):
in_kpp=770701001,
mn_okpo="12345678",
)
RegistryMembershipPeriodFactory.create(
registry=registry,
organization=organization,
started_at="2026-01-01",
ended_at=None,
)
IndustrialProductRecordFactory.create(
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
@@ -82,29 +114,100 @@ class StateCorpExchangeServiceTest(TestCase):
max_price_amount="4500000.75",
registry_organization=organization,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.ARBITRATION,
load_batch=1,
external_id="case-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
title="А40-1/2026",
record_date="2026-03-25",
status="in_progress",
payload={
"case_number": "А40-1/2026",
"court_name": "АС города Москвы",
"target": {"role": "ответчик"},
},
registry_organization=organization,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY,
load_batch=1,
external_id="fedresurs-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
title="Сообщение о намерении",
record_date="2026-03-26",
status="published",
url="https://fedresurs.ru/message/001",
payload={
"type": "Сообщение о намерении",
"date": "2026-03-26",
"case_number": "А40-555/2026",
},
registry_organization=organization,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.FAS_GOZ,
load_batch=1,
external_id="fas-goz-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
title="Уклонение от заключения контракта",
record_date="2026-02-20",
status="active",
url="https://fas.gov.ru/register/001",
payload={
"Номер реестровой записи": "ГОЗ-001",
"Полное наименование лица": organization.pn_name,
"Дата вступления постановления": "2026-02-20",
},
registry_organization=organization,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.FSTEC,
load_batch=1,
external_id="fstec-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
title="Реестр лицензий ФСТЭК",
record_date="2026-01-10",
status="present",
payload={
"registry_name": "Реестр лицензий ФСТЭК",
"entry_number": "77-001234",
"issued_at": "2026-01-10",
"expires_at": "2027-01-10",
},
registry_organization=organization,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="trudvsem-001",
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
title="Инженер-испытатель",
record_date="2026-04-01",
amount="175000.00",
status="open",
url="https://trudvsem.ru/vacancy/001",
payload={"vacancy_source": "trudvsem"},
registry_organization=organization,
)
package = StateCorpExchangeService.build_package()
package = StateCorpExchangeService.build_package(actual_date="2026-03-15")
self.assertEqual(package.payload_counts["organizations"], 1)
self.assertEqual(package.payload_counts["industrial_products"], 1)
self.assertEqual(package.payload_counts["prosecutor_checks"], 1)
self.assertEqual(package.payload_counts["public_procurements"], 1)
self.assertEqual(package.payload_counts["arbitration_cases"], 1)
self.assertEqual(package.payload_counts["bankruptcy_procedures"], 1)
self.assertEqual(package.payload_counts["defense_unreliable_suppliers"], 1)
self.assertEqual(package.payload_counts["information_security_registries"], 1)
self.assertEqual(package.payload_counts["labor_vacancies"], 1)
with ZipFile(BytesIO(package.archive_bytes)) as archive:
bin_names = [name for name in archive.namelist() if name.endswith(".bin")]
self.assertEqual(len(bin_names), 1)
bin_bytes = archive.read(bin_names[0])
header_size = struct.unpack(">I", bin_bytes[5:9])[0]
header_end = 9 + header_size
header = json.loads(bin_bytes[9:header_end].decode("utf-8"))
encrypted_payload = bin_bytes[header_end:]
raw_key = hashlib.sha256(TEST_STATE_CORP_TOKEN.encode("utf-8")).digest()
compressed_payload = AESGCM(raw_key).decrypt(
_b64url_decode(header["nonce"]),
encrypted_payload,
_b64url_decode(header["aad"]),
)
payload = json.loads(zlib.decompress(compressed_payload).decode("utf-8"))
payload = _decode_package_payload(package)
self.assertEqual(payload["format"], StateCorpExchangeService.PAYLOAD_FORMAT)
self.assertEqual(payload["manifest"]["source_system"], "mostovik")
@@ -121,7 +224,111 @@ class StateCorpExchangeServiceTest(TestCase):
payload["data"]["public_procurements"][0]["purchase_number"],
"purchase-001",
)
self.assertEqual(payload["data"]["arbitration_cases"], [])
self.assertEqual(
payload["data"]["arbitration_cases"][0]["case_number"],
"А40-1/2026",
)
self.assertEqual(
payload["data"]["bankruptcy_procedures"][0]["case_number"],
"А40-555/2026",
)
self.assertEqual(
payload["data"]["defense_unreliable_suppliers"][0]["registry_number"],
"ГОЗ-001",
)
self.assertEqual(
payload["data"]["information_security_registries"][0]["entry_number"],
"77-001234",
)
self.assertEqual(
payload["data"]["labor_vacancies"][0]["title"],
"Инженер-испытатель",
)
def test_build_package_exports_only_active_rosatom_roscosmos_registry_members(self):
target_registry = Register.objects.get(name="Реестр госкорпорации Роскосмос")
non_target_registry = RegisterFactory.create()
target = OrganizationFactory.create(
mn_inn=7707000001,
mn_ogrn=1027700000001,
pn_name="АО Целевая",
)
non_target = OrganizationFactory.create(
mn_inn=7707000002,
mn_ogrn=1027700000002,
pn_name="АО Не экспортируется",
)
inactive_target = OrganizationFactory.create(
mn_inn=7707000003,
mn_ogrn=1027700000003,
pn_name="АО Бывшая",
)
RegistryMembershipPeriodFactory.create(
registry=target_registry,
organization=target,
started_at="2026-01-01",
ended_at=None,
)
RegistryMembershipPeriodFactory.create(
registry=non_target_registry,
organization=non_target,
started_at="2026-01-01",
ended_at=None,
)
RegistryMembershipPeriodFactory.create(
registry=target_registry,
organization=inactive_target,
started_at="2026-01-01",
ended_at="2026-03-01",
)
IndustrialProductRecordFactory.create(
inn=str(target.mn_inn),
registry_number="target-product",
registry_organization=target,
)
IndustrialProductRecordFactory.create(
inn=str(non_target.mn_inn),
registry_number="non-target-product",
registry_organization=non_target,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="target-vacancy",
inn=str(target.mn_inn),
title="Целевая вакансия",
record_date="2026-03-10",
registry_organization=target,
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.TRUDVSEM,
load_batch=1,
external_id="non-target-vacancy",
inn=str(non_target.mn_inn),
title="Лишняя вакансия",
record_date="2026-03-10",
registry_organization=non_target,
)
package = StateCorpExchangeService.build_package(actual_date="2026-03-15")
payload = _decode_package_payload(package)
self.assertEqual(
[item["inn"] for item in payload["data"]["organizations"]],
[str(target.mn_inn)],
)
self.assertEqual(
[
item["registry_number"]
for item in payload["data"]["industrial_products"]
],
["target-product"],
)
self.assertEqual(
[item["external_id"] for item in payload["data"]["labor_vacancies"]],
["target-vacancy"],
)
@patch("apps.exchange.state_corp_services.requests.post")
def test_send_package_posts_multipart_archive(self, post_mock):

View File

@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
from apps.core.models import BackgroundJob, JobStatus
from apps.parsers.models import GenericParserRecord, ParserLoadLog
from apps.parsers.source_cards import (
SOURCE_CARD_DEFINITIONS,
SourceCardDefinition,
SourceCardService,
SourceItemDefinition,
@@ -18,6 +19,36 @@ from rest_framework.exceptions import ValidationError
class SourceCardServiceUnitTest(SimpleTestCase):
def test_list_cards_exposes_all_frontend_category_slugs_in_menu_order(self):
self.assertEqual(
[card.slug for card in SOURCE_CARD_DEFINITIONS],
[
"financial-indicators",
"public-procurements",
"manufacturers-and-products",
"planned-inspections",
"bankruptcy-procedures",
"defense-unreliable-suppliers",
"arbitration-cases",
"information-security-registries",
"labor-vacancies",
],
)
self.assertEqual(
[card.title for card in SOURCE_CARD_DEFINITIONS],
[
"Финансово-экономические показатели",
"Государственные закупки по 44-ФЗ и 223-ФЗ",
"Производители и продукция России",
"Плановые проверки Генпрокуратуры России",
"Сведения о процедурах банкротства",
"Недобросовестные поставщики ГОЗ",
"Арбитражные дела",
"Реестры по информационной безопасности",
"Вакансии Работа России",
],
)
def test_get_definition_raises_for_unknown_slug(self):
with self.assertRaises(Http404):
SourceCardService.get_definition("missing-card")
@@ -156,6 +187,40 @@ class SourceCardServiceUnitTest(SimpleTestCase):
},
)
@patch(
"apps.parsers.source_cards.SourceCardService._enqueue_task",
side_effect=[
{
"task_id": "task-unfair",
"task_name": "apps.parsers.tasks.parse_unfair_suppliers",
},
{
"task_id": "task-goz",
"task_name": "apps.parsers.tasks.parse_fas_goz_evasion",
},
],
)
def test_refresh_card_for_defense_unreliable_suppliers_enqueues_sources(
self, enqueue_mock
):
result = SourceCardService.refresh_card(
slug="defense-unreliable-suppliers",
requested_by_id=10,
)
self.assertEqual(result["source_card"], "defense-unreliable-suppliers")
self.assertEqual(
[item["task_id"] for item in result["tasks"]],
["task-unfair", "task-goz"],
)
self.assertEqual(
[call.kwargs["meta"]["source"] for call in enqueue_mock.call_args_list],
[
ParserLoadLog.Source.UNFAIR_SUPPLIERS,
ParserLoadLog.Source.FAS_GOZ,
],
)
def test_launch_refresh_raises_for_unsupported_card(self):
definition = SourceCardDefinition(
slug="custom-source",
@@ -297,6 +362,42 @@ class SourceCardServiceUnitTest(SimpleTestCase):
@override_settings(PARSER_STALE_LOAD_MAX_AGE_MINUTES=90)
class SourceCardServiceDatabaseTest(TestCase):
def test_defense_unreliable_suppliers_counts_unique_generic_organizations(self):
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
load_batch=1,
external_id="unfair-1",
inn="7701234567",
title="Недобросовестный поставщик",
payload={"number": "unfair-1"},
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.FAS_GOZ,
load_batch=1,
external_id="goz-1",
inn="7701234567",
title="Уклонение от ГОЗ",
payload={"number": "goz-1"},
)
ParserLoadLog.objects.create(
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
batch_id=1,
records_count=1,
status=ParserLoadLog.Status.SUCCESS,
)
ParserLoadLog.objects.create(
source=ParserLoadLog.Source.FAS_GOZ,
batch_id=1,
records_count=1,
status=ParserLoadLog.Status.SUCCESS,
)
card = SourceCardService.get_card("defense-unreliable-suppliers")
self.assertEqual(card["status"], "success")
self.assertEqual(card["records_count"], 2)
self.assertEqual(card["organizations_count"], 1)
def test_public_procurements_counts_generic_eis_sources(self):
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,

View File

@@ -249,7 +249,9 @@ class GenericSourceFetchTestCase(TestCase):
)
RegistryMembershipPeriodFactory(organization=organization, ended_at=None)
RegistryMembershipPeriodFactory(
organization=inactive, ended_at=date(2026, 5, 7)
organization=inactive,
started_at=date(2026, 5, 1),
ended_at=date(2026, 5, 7),
)
captured_inns = []
@@ -317,7 +319,9 @@ class GenericSourceFetchTestCase(TestCase):
)
RegistryMembershipPeriodFactory(organization=organization, ended_at=None)
RegistryMembershipPeriodFactory(
organization=inactive, ended_at=date(2026, 5, 7)
organization=inactive,
started_at=date(2026, 5, 1),
ended_at=date(2026, 5, 7),
)
class _CheckoClient:
@@ -448,7 +452,9 @@ class GenericSourceFetchTestCase(TestCase):
)
RegistryMembershipPeriodFactory(organization=organization, ended_at=None)
RegistryMembershipPeriodFactory(
organization=inactive, ended_at=date(2026, 5, 7)
organization=inactive,
started_at=date(2026, 5, 1),
ended_at=date(2026, 5, 7),
)
class _CheckoClient:
@@ -523,7 +529,9 @@ class GenericSourceFetchTestCase(TestCase):
)
RegistryMembershipPeriodFactory(organization=organization, ended_at=None)
RegistryMembershipPeriodFactory(
organization=inactive, ended_at=date(2026, 5, 7)
organization=inactive,
started_at=date(2026, 5, 1),
ended_at=date(2026, 5, 7),
)
class _CheckoClient:
@@ -2021,7 +2029,9 @@ class ParseVacanciesTaskTestCase(TestCase):
RegistryMembershipPeriodFactory(organization=active_first, ended_at=None)
RegistryMembershipPeriodFactory(organization=active_second, ended_at=None)
RegistryMembershipPeriodFactory(
organization=inactive, ended_at=date(2026, 5, 7)
organization=inactive,
started_at=date(2026, 5, 1),
ended_at=date(2026, 5, 7),
)
RegistryMembershipPeriodFactory(organization=active_first, ended_at=None)
captured_fetches = []

View File

@@ -9,7 +9,6 @@ from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from apps.backups.models import BackupExportJob
from apps.core.models import BackgroundJob
from apps.exchange.models import ExchangeConnection
from apps.parsers.models import (
@@ -708,19 +707,29 @@ class BackupsApiInventoryE2ETest(AuthenticatedApiMixin, APITestCase):
def setUp(self):
self.admin = UserFactory.create_superuser()
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_backup_export_endpoint(self, enqueue_mock):
@patch("apps.backups.views.StateCorpExchangeService.build_package")
def test_backup_export_endpoint(self, build_package_mock):
self.authenticate(self.admin)
today = timezone.localdate()
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": 1},
produced_at="2026-05-12T10:00:00+00:00",
)
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
reverse("api_v1:backups:export"),
{"actual_date": today.isoformat()},
format="json",
)
response = self.client.post(
reverse("api_v1:backups:export"),
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "started")
self.assertTrue(BackupExportJob.objects.filter(actual_date=today).exists())
enqueue_mock.assert_called_once()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, b"zip-bytes")
self.assertIn(
'attachment; filename="state_corp_exchange.zip"',
response["Content-Disposition"],
)
build_package_mock.assert_called_once_with(actual_date=today)