feat: complete published registry contracts and gated SRO ingestion
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s
This commit is contained in:
193
tests/apps/organizations/test_openapi_runtime_fixtures.py
Normal file
193
tests/apps/organizations/test_openapi_runtime_fixtures.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Render synthetic API responses for the generated Orval/Zod contract gate."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from apps.parsers.budget_registry import normalize_budget_record
|
||||
from apps.parsers.models import ParserSourceArtifact
|
||||
from apps.parsers.sme_support import support_payload
|
||||
from apps.parsers.sro_membership import normalize_sro_membership
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from lxml import etree
|
||||
from organizations.models import Organization, OrganizationSourceRecord
|
||||
from organizations.source_groups import get_source_group_descriptor
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from tests.apps.parsers.test_registry_payload_contract import full_budget_fixture
|
||||
from tests.apps.parsers.test_registry_snapshots import _xml
|
||||
from tests.apps.user.factories import UserFactory
|
||||
|
||||
|
||||
def _create_record(organization, source, normalized):
|
||||
descriptor = get_source_group_descriptor(source)
|
||||
extension = descriptor.extension_model.objects.create(organization=organization)
|
||||
ParserSourceArtifact.objects.create(
|
||||
source=source,
|
||||
load_batch=9001,
|
||||
status=ParserSourceArtifact.Status.PUBLISHED,
|
||||
source_published_at="2026-08-15",
|
||||
version="4.04" if source == "fns_sme_support_recipients" else "2026-08-15",
|
||||
sha256="a" * 64,
|
||||
original_name="synthetic-contract-fixture.zip",
|
||||
published_count=1,
|
||||
)
|
||||
return OrganizationSourceRecord.objects.create(
|
||||
extension=extension,
|
||||
source=source,
|
||||
record_type=descriptor.record_type,
|
||||
external_id=f"synthetic-{source}",
|
||||
load_batch=9001,
|
||||
**normalized,
|
||||
)
|
||||
|
||||
|
||||
def _canonical_records():
|
||||
organization = Organization.objects.create(
|
||||
name="АО Синтетическая организация",
|
||||
full_name="Акционерное общество Синтетическая организация",
|
||||
inn="1234567890",
|
||||
ogrn="1027700132195",
|
||||
okpo="00123456",
|
||||
directory_imported_at=timezone.now(),
|
||||
opk_registry_membership=False,
|
||||
)
|
||||
raw_budget = full_budget_fixture()
|
||||
# Unknown nested keys, leading zeroes, booleans and nulls must survive Zod.
|
||||
raw_budget["future_block"] = [
|
||||
{"future_key": {"code": "0007", "nested": [None, False, 0, {"keep": True}]}}
|
||||
]
|
||||
budget = _create_record(
|
||||
organization, "budget_ubpandnubp", normalize_budget_record(raw_budget)
|
||||
)
|
||||
|
||||
xml = etree.fromstring( # noqa: S320 - local synthetic fixture without entities
|
||||
_xml(numbers=("synthetic-support",), units=("1", "2", "3", "4", "5"))
|
||||
)
|
||||
payload, amount = support_payload(
|
||||
"synthetic-document", xml.find("Документ/СвПредПод"), "2026-08-15"
|
||||
)
|
||||
sme = _create_record(
|
||||
organization,
|
||||
"fns_sme_support_recipients",
|
||||
{
|
||||
"title": "Синтетическая мера поддержки",
|
||||
"record_date": payload["decision_date"],
|
||||
"amount": amount,
|
||||
"status": "",
|
||||
"url": "https://www.nalog.gov.ru/opendata/7707329152-rsmppp/",
|
||||
"payload": payload,
|
||||
},
|
||||
)
|
||||
|
||||
sro = _create_record(
|
||||
organization,
|
||||
"sro_membership_check",
|
||||
normalize_sro_membership(
|
||||
{
|
||||
"name": organization.name,
|
||||
"full_name": organization.full_name,
|
||||
"inn": organization.inn,
|
||||
"ogrn": organization.ogrn,
|
||||
"region": "Синтетический регион",
|
||||
"sro_name": "Синтетическая СРО",
|
||||
"membership_status_raw": "Является членом",
|
||||
},
|
||||
organization,
|
||||
sro_id="90001",
|
||||
sro_url="https://www.reestr-sro.ru/test-fixtures/sro-id-90001/",
|
||||
source_url="https://www.reestr-sro.ru/test-fixtures/lookup/",
|
||||
source_registry_date="2026-08-15",
|
||||
admitted=None,
|
||||
missing_reason="not_found",
|
||||
resolution="row_link",
|
||||
lookup_key="ogrn",
|
||||
lookup_value=organization.ogrn,
|
||||
),
|
||||
)
|
||||
return [budget, sme, sro], raw_budget
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_source_record_runtime_fixtures():
|
||||
client = APIClient()
|
||||
client.force_authenticate(UserFactory.create_user())
|
||||
records, raw_budget = _canonical_records()
|
||||
list_url = reverse("api_v2:organizations:organization-source-records-list")
|
||||
cases = []
|
||||
|
||||
def capture(name, operation, response, status=200):
|
||||
assert response.status_code == status, (name, response.status_code)
|
||||
# Rendered JSON is the actual wire representation, not DRF's .data.
|
||||
body = response.json()
|
||||
cases.append(
|
||||
{"name": name, "operation": operation, "status": status, "body": body}
|
||||
)
|
||||
return body
|
||||
|
||||
mixed = capture("mixed-list", "list", client.get(list_url))
|
||||
assert len(mixed["data"]) == 3
|
||||
assert mixed["meta"]["pagination"]["total_count"] == 3
|
||||
details = {}
|
||||
for record in records:
|
||||
listing = capture(
|
||||
f"{record.source}-list",
|
||||
"list",
|
||||
client.get(list_url, {"source": record.source}),
|
||||
)
|
||||
assert len(listing["data"]) == 1
|
||||
detail_url = reverse(
|
||||
"api_v2:organizations:organization-source-records-detail",
|
||||
kwargs={"uid": record.uid},
|
||||
)
|
||||
details[record.source] = capture(
|
||||
f"{record.source}-detail", "detail", client.get(detail_url)
|
||||
)
|
||||
budget_payload = details["budget_ubpandnubp"]["payload"]
|
||||
assert budget_payload["upstream"] == raw_budget
|
||||
assert (
|
||||
budget_payload["unclassified_blocks"]["future_block"]
|
||||
== raw_budget["future_block"]
|
||||
)
|
||||
assert len(details["fns_sme_support_recipients"]["payload"]["support_sizes"]) == 5
|
||||
assert details["sro_membership_check"]["payload"]["admission_date"] is None
|
||||
|
||||
empty = capture(
|
||||
"empty-list", "list", client.get(list_url, {"search": "absent-fixture-value"})
|
||||
)
|
||||
assert empty["data"] == []
|
||||
assert empty["meta"]["pagination"]["total_count"] == 0
|
||||
assert empty["meta"]["pagination"]["total_pages"] == 0
|
||||
capture(
|
||||
"invalid-list",
|
||||
"list",
|
||||
client.get(list_url, {"page_size": "broken"}),
|
||||
400,
|
||||
)
|
||||
for name, uid, status in (
|
||||
("invalid-detail", "broken", 400),
|
||||
("missing-detail", str(uuid4()), 404),
|
||||
):
|
||||
capture(
|
||||
name,
|
||||
"detail",
|
||||
client.get(
|
||||
reverse(
|
||||
"api_v2:organizations:organization-source-records-detail",
|
||||
kwargs={"uid": uid},
|
||||
)
|
||||
),
|
||||
status,
|
||||
)
|
||||
|
||||
# Ordinary pytest runs create no artifact. CI opts in with an explicit path.
|
||||
destination = os.environ.get("OPENAPI_RUNTIME_FIXTURES")
|
||||
if destination:
|
||||
Path(destination).write_text(
|
||||
json.dumps({"version": 1, "cases": cases}, ensure_ascii=False, indent=2)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
Reference in New Issue
Block a user