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
194 lines
6.9 KiB
Python
194 lines
6.9 KiB
Python
"""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",
|
||
)
|