Files
mostovik-backend/tests/apps/parsers/test_registry_payload_contract.py
Aleksandr Meshchryakov 18971d33ec
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
feat: complete published registry contracts and gated SRO ingestion
2026-09-14 17:01:02 +02:00

728 lines
30 KiB
Python

"""Source-first payload contracts; all upstream input is local synthetic evidence."""
import json
from copy import deepcopy
from io import StringIO
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from apps.parsers.budget_registry import refresh_budget_registry
from apps.parsers.models import ParserSourceArtifact
from apps.parsers.sme_support import import_sme_support, support_payload
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase, override_settings
from django.utils import timezone
from lxml import etree
from organizations.budget_payload import budget_list_payload, normalize_budget_payload
from organizations.models import Organization, OrganizationSourceRecord
from organizations.registry_payload_serializers import (
BudgetRegistryRecordDetailPayloadSerializer,
BudgetRegistryRecordListPayloadSerializer,
RegistryCodeNameSerializer,
SmeSupportRecordDetailPayloadSerializer,
SmeSupportRecordListPayloadSerializer,
)
from organizations.registry_payload_values import registry_boolean, registry_date
from organizations.registry_payloads import (
build_registry_payload_context,
serialize_registry_payload,
)
from organizations.sme_payload import (
VIOLATION_LABELS,
normalize_sme_payload,
sme_list_payload,
)
from tests.apps.parsers.test_registry_snapshots import _archive, _budget_record, _xml
def full_budget_fixture():
value = _budget_record()
value["info"].update(
{
"orgTypeCode": "03",
"orgTypeName": "Учреждение",
"isObosob": "false",
"isOGV": "0",
"isUch": "1",
"isReorg": "unobserved",
"inclusionDate": "2025-11-13 18:44:39.0",
"regDate": "30.12.2017",
"regionCode": "01",
"regionName": "Регион",
"regNum": "00017",
"newInfoField": "raw-kept",
}
)
value.update(
{
"heads": [
{
"fio": "Руководитель",
"post": "Директор",
"headMain": "1",
"docDate": "2024-01-02",
}
],
"contacts": [
{
"phone": "123",
"mail": "fixture@example.test",
"site": "https://example.test",
}
],
"activities": [
{
"activityCode": "01.01",
"activityName": "Деятельность",
"activityKind": "основной",
}
],
"authorities": [
{
"authorityCode": "001",
"authorityName": "Орган",
"permissions": [
{"permissionCode": "002", "permissionName": "Полномочие"}
],
}
],
"participantPermissions": [
{"code": "001", "name": "Участник", "startDate": "2024-01-02"}
],
"nonParticipantPermissions": [
{
"code": "002",
"registryNum": "00077",
"authBudgCode": "01",
"authBudgName": "Бюджет",
}
],
"procurementPermissions": [{"code": "201", "name": "заказчик"}],
"acceptAuths": [
{"authRegNum": "009", "authGiverCode": "007", "authGiverName": "Орган"}
],
"transfauth": [
{
"authregnum": "004",
"authfomunicipalcode": "001",
"authfomunicipalname": "Район",
}
],
"ubptransfauthbp": [
{"budgetnsicode": "001", "budgetnsiname": "Бюджет", "codebk": "009"}
],
"facialAccounts": [
{
"num": "000123",
"kindCode": "001",
"kindName": "Лицевой",
"createDate": "2020-01-01",
"accountorgcode": "002",
"accountorgfullname": "Организация",
}
],
"foAccounts": [{"num": "000456", "foCode": "009", "foName": "Орган"}],
"ksaccounts": [
{
"num": "000789",
"opendate": "2020-02-03",
"opentofkcode": "006",
"opentofkname": "Казначейство",
}
],
"successions": [
{
"numberdoc": "00002",
"documentdate": "2020-01-02",
"parentCode": "001",
}
],
"contracts": [{"contractnumber": "00003", "signdate": "2024-01-02"}],
"attachment": [{"unobserved": {"keep": ["0001", None]}}],
"ubpfin": [],
}
)
return value
def test_budget_partial_nullable_empty_collections_and_strict_booleans():
payload = normalize_budget_payload({"info": {}})
assert payload["registry"]["registration_number"] is None
assert payload["classification"]["organization_type"] is None
assert payload["is_separate_division"] is None
assert all(value == [] for value in payload["accounts"].values())
assert all(value == [] for value in payload["permissions"].values())
assert payload["summary"]["accounts_count"] == 0
assert payload["summary"]["has_procurement_permission"] is False
assert BudgetRegistryRecordDetailPayloadSerializer(payload).data == payload
assert BudgetRegistryRecordListPayloadSerializer(
budget_list_payload(payload)
).data == budget_list_payload(payload)
def test_full_budget_blocks_strings_lineage_and_list_detail_agreement():
raw = full_budget_fixture()
before = deepcopy(raw)
payload = normalize_budget_payload(raw)
assert raw == before
assert payload["upstream"] == before
assert payload["registry"]["inclusion_date"] == "2025-11-13T18:44:39Z"
assert payload["registry"]["registration_date"] == "2017-12-30"
assert payload["address"]["full"] == "Регион"
assert payload["classification"]["flags"]["is_reorganized"] is None
assert payload["accounts"]["personal"][0]["number"] == "000123"
assert payload["accounts"]["financial_authority"][0]["number"] == "000456"
assert payload["accounts"]["treasury"][0]["number"] == "000789"
assert payload["permissions"]["non_participant"][0]["registry_number"] == "00077"
assert payload["summary"] == {
"activities_count": 1,
"authorities_count": 1,
"permissions_count": 6,
"accounts_count": 3,
"successions_count": 1,
"has_procurement_permission": True,
}
assert payload["unclassified_blocks"]["attachment"] == raw["attachment"]
assert payload["unclassified_blocks"]["info"] == [{"newInfoField": "raw-kept"}]
assert payload["schema_drift"]["unclassified_blocks_count"] == 3
assert {item["json_path"] for item in payload["schema_drift"]["lineage"]} == {
"$.attachment",
"$.info",
"$.unknown_new_block",
}
assert BudgetRegistryRecordDetailPayloadSerializer(payload).data == payload
listing = budget_list_payload(payload)
assert "upstream" not in listing and "accounts" not in listing
assert listing["registry"] == {
key: payload["registry"][key] for key in listing["registry"]
}
for name, field in BudgetRegistryRecordDetailPayloadSerializer().fields.items():
assert field.required, name
def test_large_budget_detail_keeps_every_collection_item():
raw = full_budget_fixture()
raw["successions"] = [{"numberdoc": f"{i:08d}"} for i in range(2000)]
payload = normalize_budget_payload(raw)
assert (
len(payload["successions"]) == payload["summary"]["successions_count"] == 2000
)
assert payload["successions"][-1]["document_number"] == "00001999"
assert "successions" not in budget_list_payload(payload)
def test_incomplete_budget_dictionary_is_null_with_raw_lineage_and_metric():
raw = {"info": {"orgTypeCode": "003", "budgetLvlName": "Бюджет"}}
payload = normalize_budget_payload(raw)
assert payload["classification"]["organization_type"] is None
assert payload["budget"]["level"] is None
assert payload["upstream"] == raw
assert set(payload["schema_drift"]["incomplete_dictionaries"]) == {
"$.info.orgTypeCode",
"$.info.budgetLvlCode",
}
assert not RegistryCodeNameSerializer(data={"code": "003", "name": None}).is_valid()
assert RegistryCodeNameSerializer(
data={"code": "003", "name": "Учреждение"}
).is_valid()
record = SimpleNamespace(
source="budget_ubpandnubp",
payload={"classification": {"organization_type": {"code": "003", "name": ""}}},
)
assert (
serialize_registry_payload(record, detail=False)["classification"][
"organization_type"
]
is None
)
def test_partial_authority_permission_retains_raw_without_invalid_dictionary():
raw = {
"info": {"code": "001"},
"authorities": [
{
"authorityCode": "001",
"authorityName": "Орган",
"permissions": [
{"permissionCode": "0002"},
{"permissionCode": "0003", "permissionName": "Полномочие"},
],
}
],
}
payload = normalize_budget_payload(raw)
assert payload["authorities"][0]["permissions"] == [
{"code": "0003", "name": "Полномочие"}
]
assert payload["unclassified_blocks"]["authorities"] == raw["authorities"]
assert payload["upstream"] == raw
assert payload["schema_drift"]["incomplete_dictionaries"] == [
"$.authorities[0].permissions[0]"
]
serializer = BudgetRegistryRecordDetailPayloadSerializer(data=payload)
assert serializer.is_valid(), serializer.errors
def test_partial_budget_without_raw_preserves_existing_list_and_detail_values():
partial = {
"registry": {"code": "0001", "registration_number": "0002"},
"is_separate_division": True,
"summary": {"activities_count": 7, "has_procurement_permission": True},
"contacts": [{"phone": "123", "email": None, "website": None}],
"classification": {"organization_type": {"code": "03", "name": "Учреждение"}},
"unclassified_blocks": {"retained": [{"code": "001"}]},
}
record = SimpleNamespace(source="budget_ubpandnubp", payload=partial)
before = deepcopy(partial)
detail = serialize_registry_payload(record, detail=True)
assert detail["is_separate_division"] is True
assert detail["summary"]["activities_count"] == 7
assert detail["summary"]["has_procurement_permission"] is True
assert detail["summary"]["accounts_count"] == 0
assert detail["contacts"] == partial["contacts"]
assert detail["unclassified_blocks"] == partial["unclassified_blocks"]
assert detail["registry"]["registration_number"] == "0002"
assert partial == before
assert serialize_registry_payload(record, detail=False) == budget_list_payload(
detail
)
def test_partial_budget_raw_falls_back_only_for_missing_upstream_fields():
raw = {"id": "source-id", "info": {"statusCode": "2"}}
payload = {
"registry": {"code": "0001", "registration_number": "0002"},
"budget": {"name": "Известный бюджет"},
"address": {"region": {"code": "77", "name": "Москва"}},
"classification": {"organization_type": {"code": "03", "name": "Учреждение"}},
"upstream": raw,
}
record = SimpleNamespace(source="budget_ubpandnubp", payload=payload)
original = deepcopy(payload)
detail = serialize_registry_payload(record, detail=True)
assert detail["registry"]["code"] == "0001"
assert detail["registry"]["registration_number"] == "0002"
assert detail["budget"]["name"] == "Известный бюджет"
assert detail["address"]["region"] == {"code": "77", "name": "Москва"}
assert detail["upstream"] == raw and record.payload == original
raw["info"].update(regNum=None, budgetName="", regionCode="78", orgTypeName=None)
detail = serialize_registry_payload(record, detail=True)
assert detail["registry"]["registration_number"] is None
assert detail["budget"]["name"] is None
assert detail["address"]["region"] is None
assert detail["classification"]["organization_type"] is None
assert serialize_registry_payload(record, detail=False) == budget_list_payload(
detail
)
def test_budget_registry_code_is_required_and_never_substituted_from_id():
from apps.parsers.budget_registry import normalize_budget_record
from apps.parsers.registry_snapshots import SnapshotValidationError
raw = full_budget_fixture()
for missing in (None, "", ""):
raw["info"]["code"] = missing
with pytest.raises(
SnapshotValidationError, match="missing_budget_registry_code"
):
normalize_budget_record(raw)
serializer = BudgetRegistryRecordListPayloadSerializer(
data=budget_list_payload(normalize_budget_payload(raw))
)
assert not serializer.is_valid()
assert "code" in serializer.errors["registry"]
def test_named_payload_openapi_schemas_have_required_typed_components():
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema
from rest_framework.views import APIView
components = openapi.ReferenceResolver(openapi.SCHEMA_DEFINITIONS, force_init=True)
inspector = SwaggerAutoSchema(APIView(), "/fixture/", "GET", components, None, {})
for schema in (
BudgetRegistryRecordDetailPayloadSerializer,
SmeSupportRecordDetailPayloadSerializer,
):
inspector.serializer_to_schema(schema())
budget = components.get(
"BudgetRegistryRecordDetailPayload", scope=openapi.SCHEMA_DEFINITIONS
)
sme = components.get(
"SmeSupportRecordDetailPayload", scope=openapi.SCHEMA_DEFINITIONS
)
assert {
"registry",
"accounts",
"summary",
"permissions",
"unclassified_blocks",
} <= set(budget["required"])
assert {"support_sizes", "provider", "violations", "provenance"} <= set(
sme["required"]
)
assert budget["properties"]["accounts"]["$ref"].endswith("/BudgetAccounts")
assert sme["properties"]["violations"]["items"]["$ref"].endswith(
"/SmeSupportViolation"
)
assert budget["properties"]["upstream"]["additionalProperties"] is True
assert (
budget["properties"]["unclassified_blocks"]["additionalProperties"]["items"][
"additionalProperties"
]
is True
)
assert sme["properties"]["upstream"]["additionalProperties"] is True
unclassified = components.get(
"BudgetUnclassifiedItem", scope=openapi.SCHEMA_DEFINITIONS
)
assert unclassified["additionalProperties"] is True
@pytest.mark.parametrize(
"raw,expected",
[
("0", False),
("1", True),
("FALSE", False),
("true", True),
(None, None),
("unknown", None),
],
)
def test_registry_boolean_is_strict(raw, expected):
assert registry_boolean(raw) is expected
def test_registry_timestamps_use_explicit_utc_application_rule_and_preserve_dates():
assert (
registry_date("2025-01-02 03:04:05.0", timestamp=True) == "2025-01-02T03:04:05Z"
)
assert (
registry_date("2025-01-02T03:04:05+03:00", timestamp=True)
== "2025-01-02T00:04:05Z"
)
assert registry_date("2025-01-02", timestamp=True) == "2025-01-02T00:00:00Z"
assert registry_date("2025-01-02T03:04:05+03:00") == "2025-01-02"
with pytest.raises(ValueError):
registry_date("2025-02-30")
def test_sme_mixed_sizes_documents_violations_and_read_compatibility():
root = etree.fromstring(_xml(units=("1", "2", "3", "4", "5"))) # noqa: S320 -- local fixture only
measure = root.find("Документ/СвПредПод")
measure.find("Нарушения").set("СрокНаруш", "01.04.2025")
payload, amount = support_payload("doc", measure, "2026-08-15")
assert amount == "2.50"
assert [size["unit"] for size in payload["support_sizes"]] == [
"RUB",
"square_meter",
"hour",
"percent",
"unit",
]
assert payload["recipient_type"] == {"code": "1", "name": "Юридическое лицо"}
assert payload["sme_category"] == {"code": "1", "name": "Микропредприятие"}
assert payload["violations"][0] == {
"type": {"code": "1", "name": VIOLATION_LABELS["1"]},
"recognized_date": "2025-02-01",
"remedy_deadline": "2025-04-01",
"remedied_date": None,
}
assert [doc["number"] for doc in payload["regulatory_documents"]] == ["A", "B"]
assert payload["regulatory_documents"][0]["date"] is None
assert (
payload["violation_count"] == 1 and payload["regulatory_documents_count"] == 2
)
assert payload["upstream"]["violations"][0]["ВидНаруш"] == "1"
assert SmeSupportRecordDetailPayloadSerializer(payload).data == payload
assert SmeSupportRecordListPayloadSerializer(
sme_list_payload(payload)
).data == sme_list_payload(payload)
assert normalize_sme_payload(payload) == payload
@pytest.mark.parametrize(
"recipient,category,violation",
[("1", "1", "1"), ("2", "2", "2"), ("3", "3", "1"), ("4", "4", "2")],
)
def test_official_fns_dictionary_codes_have_names(recipient, category, violation):
root = etree.fromstring(_xml()) # noqa: S320 -- local fixture only
measure = root.find("Документ/СвПредПод")
measure.set("ВидПП", recipient)
measure.set("КатСуб", category)
measure.find("Нарушения").set("ВидНаруш", violation)
payload, _ = support_payload("doc", measure, "2026-08-15")
for name in ("recipient_type", "sme_category"):
assert payload[name]["name"] and payload[name]["name"] != payload[name]["code"]
if recipient == "3":
assert "Налог на профессиональный доход" in payload["recipient_type"]["name"]
if recipient == "4":
assert "Глава крестьянского" in payload["recipient_type"]["name"]
assert payload["sme_category"]["name"] == "Отсутствует"
assert payload["violations"][0]["type"]["name"] == VIOLATION_LABELS[violation]
def test_unknown_sme_dictionary_is_not_published_with_invented_label():
root = etree.fromstring(_xml()) # noqa: S320 -- local fixture only
measure = root.find("Документ/СвПредПод")
measure.set("КатСуб", "9")
with pytest.raises(ValueError, match="unknown_sme_dictionary_code"):
support_payload("doc", measure, "2026-08-15")
def test_sme_compatibility_normalizes_optional_regions_and_official_labels():
root = etree.fromstring(_xml()) # noqa: S320 -- local fixture only
payload, _ = support_payload("doc", root.find("Документ/СвПредПод"), "2026-08-15")
payload["region"] = {"code": "01", "name": None}
payload["provider"]["region"] = {"code": None, "name": "Регион"}
payload["recipient_type"]["name"] = "Устаревшее имя"
normalized = normalize_sme_payload(payload)
assert normalized["region"] is None
assert normalized["provider"]["region"] is None
assert normalized["recipient_type"]["name"] == "Юридическое лицо"
serializer = SmeSupportRecordDetailPayloadSerializer(data=normalized)
assert serializer.is_valid(), serializer.errors
payload["recipient_type"] = {"code": "9", "name": "Неофициальное значение"}
with pytest.raises(ValueError, match="unknown_sme_dictionary_code"):
normalize_sme_payload(payload)
class RegistryPayloadContextTest(TestCase):
def setUp(self):
self.storage = TemporaryDirectory(prefix="registry-payload-test-")
self.addCleanup(self.storage.cleanup)
self.settings_override = override_settings(MEDIA_ROOT=self.storage.name)
self.settings_override.enable()
self.addCleanup(self.settings_override.disable)
Organization.objects.create(
name="Получатель",
inn="1234567890",
ogrn="1027700132195",
okpo="00123456",
directory_imported_at=timezone.now(),
)
self.provider = Organization.objects.create(
name="Каноническое имя",
inn="9876543210",
ogrn="1027700132195",
okpo="00012345",
directory_imported_at=timezone.now(),
)
self.artifact, _ = import_sme_support(
handle=_archive(_xml()),
original_name="fixture.zip",
snapshot_date="2026-08-15",
load_batch=1,
)
def test_page_context_is_bounded_and_preserves_upstream_provider(self):
records = list(
OrganizationSourceRecord.objects.select_related("extension__organization")
)
with self.assertNumQueries(2):
context = build_registry_payload_context(records * 25)
with self.assertNumQueries(0):
for record in records:
data = serialize_registry_payload(record, detail=True, context=context)
self.assertEqual(data["provider"]["name"], "Поставщик")
self.assertEqual(
data["provider"]["organization_uid"], str(self.provider.uid)
)
self.assertEqual(data["provider"]["okpo"], "00012345")
self.assertIsNone(data["region"])
self.assertEqual(
data["provenance"]["archive_sha256"], self.artifact.sha256
)
self.assertEqual(data, record.payload)
def test_old_sme_payload_and_budget_raw_get_current_shape_without_write(self):
record = OrganizationSourceRecord.objects.first()
old = deepcopy(record.payload)
old["recipient_type"] = "1"
old["sme_category"] = "1"
old["violations"] = deepcopy(old["upstream"]["violations"])
old["regulatory_documents"] = deepcopy(old["upstream"]["regulatory_documents"])
record.payload = old
context = build_registry_payload_context([record])
with self.assertNumQueries(0):
normalized = serialize_registry_payload(
record, detail=True, context=context
)
self.assertIsInstance(normalized["recipient_type"], dict)
self.assertEqual(record.payload, old)
raw = full_budget_fixture()
old_budget = SimpleNamespace(
source="budget_ubpandnubp", payload={"upstream": raw}
)
with self.assertNumQueries(0):
self.assertEqual(
serialize_registry_payload(old_budget, detail=True),
normalize_budget_payload(raw),
)
def legacy_records(self):
records = list(OrganizationSourceRecord.objects.order_by("uid"))
for record in records:
record.payload["recipient_type"] = "1"
record.payload["sme_category"] = "1"
record.status = ""
record.save(update_fields=["payload", "status"])
return records
def test_backfill_dry_run_default_then_apply_is_atomic_and_idempotent(self):
records = self.legacy_records()
before = {
record.uid: (deepcopy(record.payload), record.status, record.updated_at)
for record in records
}
command_module = (
"organizations.management.commands.normalize_published_registry_payloads"
)
output = StringIO()
with patch(f"{command_module}.invalidate_source_data_cache") as invalidate:
dry = json.loads(
call_command(
"normalize_published_registry_payloads", stdout=output, silent=True
)
)
assert dry["dry_run"] and dry["changed"] == 2
invalidate.assert_not_called()
for record in records:
record.refresh_from_db()
assert (record.payload, record.status, record.updated_at) == before[
record.uid
]
with self.captureOnCommitCallbacks(execute=True):
applied = json.loads(
call_command(
"normalize_published_registry_payloads",
apply=True,
stdout=output,
silent=True,
)
)
assert not applied["dry_run"] and applied["changed"] == 2
assert applied["exports_rebuild_required"]
assert invalidate.call_count == 2
for record in records:
record.refresh_from_db()
assert record.status == "published"
assert record.payload["sme_category"]["code"] == "1"
assert record.updated_at == before[record.uid][2]
repeated = json.loads(
call_command(
"normalize_published_registry_payloads",
apply=True,
stdout=output,
silent=True,
)
)
assert repeated["changed"] == 0
def test_backfill_late_invalid_record_rolls_back_earlier_batch(self):
records = self.legacy_records()
records[-1].payload["support_sizes"] = []
records[-1].save(update_fields=["payload"])
with pytest.raises(CommandError):
call_command(
"normalize_published_registry_payloads",
apply=True,
batch_size=1,
stdout=StringIO(),
stderr=StringIO(),
silent=True,
)
records[0].refresh_from_db()
assert records[0].status == "" and records[0].payload["sme_category"] == "1"
def test_backfill_cache_failure_rolls_back_payload_and_status(self):
records = self.legacy_records()
with patch(
"organizations.management.commands.normalize_published_registry_payloads.invalidate_source_data_cache",
side_effect=RuntimeError("cache unavailable"),
), pytest.raises(CommandError):
call_command(
"normalize_published_registry_payloads",
apply=True,
stdout=StringIO(),
stderr=StringIO(),
silent=True,
)
for record in records:
record.refresh_from_db()
assert record.status == "" and record.payload["sme_category"] == "1"
def test_post_commit_cache_failure_reports_committed_state(self):
records = self.legacy_records()
module = (
"organizations.management.commands.normalize_published_registry_payloads"
)
with patch(
f"{module}.invalidate_source_data_cache",
side_effect=[None, RuntimeError("cache unavailable")],
), patch(
f"{module}.transaction.on_commit", side_effect=lambda callback: callback()
), pytest.raises(CommandError, match="updates committed"):
call_command(
"normalize_published_registry_payloads",
apply=True,
stdout=StringIO(),
stderr=StringIO(),
silent=True,
)
for record in records:
record.refresh_from_db()
assert record.status == "published"
def test_budget_import_persists_unknown_block_metric_and_page_version(self):
raw = full_budget_fixture()
raw["info"]["okpoCode"] = "00123456"
page = {"data": [raw], "recordCount": 1, "pageNum": 1, "version": "10"}
with patch("apps.parsers.budget_registry.budget_page", return_value=page):
artifact, _ = refresh_budget_registry(load_batch=2, session=object())
assert artifact.metadata["schema_drift_records_count"] == 1
assert artifact.metadata["schema_drift_blocks"] == {
"attachment": 1,
"info": 1,
"unknown_new_block": 1,
}
record = OrganizationSourceRecord.objects.get(source="budget_ubpandnubp")
assert record.payload["upstream_audit"]["source_version"] == "10"
with self.assertNumQueries(1):
context = build_registry_payload_context([record])
with self.assertNumQueries(0):
assert (
serialize_registry_payload(record, detail=True, context=context)
== record.payload
)
def test_budget_missing_required_code_keeps_last_published_snapshot(self):
from apps.parsers.registry_snapshots import SnapshotValidationError
raw = full_budget_fixture()
raw["info"]["okpoCode"] = "00123456"
page = {"data": [raw], "recordCount": 1, "pageNum": 1, "version": "10"}
with patch("apps.parsers.budget_registry.budget_page", return_value=page):
refresh_budget_registry(load_batch=2, session=object())
before = OrganizationSourceRecord.objects.get(source="budget_ubpandnubp")
del raw["info"]["code"]
with patch(
"apps.parsers.budget_registry.budget_page", return_value=page
), pytest.raises(SnapshotValidationError, match="missing_budget_registry_code"):
refresh_budget_registry(load_batch=3, session=object())
current = OrganizationSourceRecord.objects.get(source="budget_ubpandnubp")
assert (current.uid, current.payload) == (before.uid, before.payload)
assert (
ParserSourceArtifact.objects.get(
source="budget_ubpandnubp", load_batch=3
).status
== "rejected"
)