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

@@ -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):