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