Files
mostovik-backend/tests/apps/organizations/test_registry_query_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

401 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Source-specific filters and pagination operate on the published dataset."""
from uuid import UUID
import pytest
from django.urls import reverse
from organizations.models import (
BudgetProcessRegistryExtension,
GovernmentSupportExtension,
Organization,
OrganizationSourceRecord,
SroMembershipExtension,
)
from rest_framework.test import APIClient
from tests.apps.user.factories import UserFactory
@pytest.fixture
def registry_query(db):
client = APIClient()
client.force_authenticate(UserFactory.create_user())
organization = Organization.objects.create(
name="Организация реестра",
inn="7707083810",
ogrn="1027700132195",
okpo="00123456",
opk_registry_membership=False,
)
budget = BudgetProcessRegistryExtension.objects.create(organization=organization)
support = GovernmentSupportExtension.objects.create(organization=organization)
records = []
for i, region, branch in ((1, "77", True), (2, "50", False), (3, None, None)):
records.append(
OrganizationSourceRecord.objects.create(
uid=UUID(int=i),
extension=budget,
source="budget_ubpandnubp",
record_type="budget_registry_organization",
external_id=f"000{i}",
title="Бюджетная организация",
record_date="2026-09-14",
status="active" if i == 1 else "unknown",
payload={
"registry": {"code": f"REG-{i}"},
"address": {"region": {"code": region, "name": region}},
"classification": {
"organization_type": {"code": "03", "name": "Казённое"}
},
"budget": {"level": {"code": "10", "name": "Федеральный"}},
"is_separate_division": branch,
"summary": {"has_procurement_permission": i == 1},
},
)
)
for i, amount, unit in (
(4, "120.50", "RUB"),
(5, None, "hour"),
(6, "900.00", "RUB"),
):
records.append(
OrganizationSourceRecord.objects.create(
uid=UUID(int=i),
extension=support,
source="fns_sme_support_recipients",
record_type="sme_support_measure",
external_id=f"MEASURE-{i}",
title="Поддержка",
amount=amount,
record_date="2026-09-14",
payload={
"support_registry_number": f"000{i}",
"recipient_type": "1",
"sme_category": {"code": "1", "name": "Микропредприятие"},
"support_form": {"code": "01", "name": "Финансовая"},
"support_kind": {"code": "07", "name": "Субсидия"},
"provider": {"inn": "7707083810", "name": "Поставщик помощи"},
"has_violation": i == 4,
"support_sizes": [
{
"unit_code": "1" if unit == "RUB" else "3",
"unit": unit,
"value": amount or "8.00",
}
],
"registry_entry_date": "2026-09-14",
"decision_date": "2026-09-14",
"source_snapshot_date": "2026-09-14",
"support_until": "2027-09-14",
"termination_date": None,
},
)
)
return client, records
def get_records(client, **params):
return client.get(
reverse("api_v2:organizations:organization-source-records-list"), params
)
def ids(response):
assert response.status_code == 200, response.data
return [UUID(row["uid"]).int for row in response.data["data"]]
def test_registry_scope_includes_own_non_opk_and_typed_pagination(registry_query):
client, _ = registry_query
response = get_records(
client, source_group="budget_process_registry", ordering="uid"
)
assert ids(response) == [1, 2, 3]
assert response.data["meta"]["pagination"] == {
"page": 1,
"page_size": 50,
"total_count": 3,
"total_pages": 1,
"has_next": False,
"has_previous": False,
}
empty = get_records(
client, source_group="budget_process_registry", region_code="99"
)
assert ids(empty) == []
assert empty.data["meta"]["pagination"]["total_pages"] == 0
@pytest.mark.parametrize(
("params", "expected"),
[
({"region_code": "77"}, [1]),
({"is_branch": "false"}, [2]),
({"has_procurement_permission": "true"}, [1]),
({"organization_type": "03", "budget_level": "10"}, [1, 2, 3]),
({"search": "REG-2"}, [2]),
],
)
def test_budget_filters_and_registry_search(registry_query, params, expected):
client, _ = registry_query
assert (
ids(
get_records(
client, source_group="budget_process_registry", ordering="uid", **params
)
)
== expected
)
@pytest.mark.parametrize(
("params", "expected"),
[
({"support_unit": "hour"}, [5]),
({"amount_from": "120.50", "amount_to": "120.50"}, [4]),
({"has_violation": "true"}, [4]),
(
{"sme_category": "1", "support_form_code": "01", "support_kind_code": "07"},
[4, 5, 6],
),
(
{"support_form": "01", "support_kind": "07", "provider_inn": "7707083810"},
[4, 5, 6],
),
(
{"registry_entry_from": "2026-09-14", "support_until_to": "2027-09-14"},
[4, 5, 6],
),
({"search": "Поставщик помощи"}, [4, 5, 6]),
({"termination_from": "2020-01-01"}, []),
],
)
def test_sme_filters_units_amounts_dates_and_search(registry_query, params, expected):
client, _ = registry_query
assert (
ids(
get_records(
client, source_group="government_support", ordering="uid", **params
)
)
== expected
)
@pytest.mark.parametrize(
"ordering", ["payload__is_separate_division", "-payload__is_separate_division"]
)
def test_budget_boolean_ordering_null_last_and_uid_ties(registry_query, ordering):
client, _ = registry_query
expected = [2, 1, 3] if not ordering.startswith("-") else [1, 2, 3]
response = get_records(
client, source_group="budget_process_registry", ordering=ordering
)
assert ids(response) == expected
@pytest.mark.parametrize(
"params",
[
{"unknown_filter": "yes"},
{"is_branch": "perhaps"},
{"provider_inn": "123"},
{"organization": "not-a-uuid"},
{"search": "x" * 201},
{"amount_from": "NaN"},
{"amount_from": "2", "amount_to": "1"},
{"registry_entry_from": "2026-02-30"},
{"support_until_from": "2026-02-01", "support_until_to": "2026-01-01"},
{"support_form_code": "1", "support_form": "2"},
{"support_unit": "dollar"},
{"page_size": "101"},
{"ordering": "payload__secret"},
],
)
def test_invalid_filters_return_machine_readable_400(registry_query, params):
client, _ = registry_query
response = get_records(client, source_group="government_support", **params)
assert response.status_code == 400, response.data
assert response.data["errors"][0]["code"]
def test_filtering_and_ordering_precede_pagination(registry_query):
client, _ = registry_query
response = get_records(
client,
source_group="government_support",
support_unit="RUB",
ordering="amount",
page_size=1,
page=2,
)
assert ids(response) == [6]
assert response.data["meta"]["pagination"]["total_count"] == 2
@pytest.mark.parametrize("field", ["name", "full_name", "inn", "ogrn", "okpo"])
@pytest.mark.parametrize("prefix", ["organization__", "extension__organization__"])
@pytest.mark.parametrize("descending", [False, True])
def test_organization_ordering_keeps_public_nulls_last(
registry_query, field, prefix, descending
):
client, records = registry_query
Organization.objects.filter(pk=records[0].extension.organization_id).update(
**{field: ""}
)
organization = Organization.objects.create(
name="Заполненная организация",
full_name="Полное наименование",
inn="7700000001",
ogrn="1027700000001",
okpo="00000001",
)
extension = BudgetProcessRegistryExtension.objects.create(organization=organization)
OrganizationSourceRecord.objects.create(
uid=UUID(int=100),
extension=extension,
source="budget_ubpandnubp",
record_type="budget_registry_organization",
external_id="filled",
payload={},
)
response = get_records(
client,
source_group="budget_process_registry",
ordering=("-" if descending else "") + prefix + field,
)
assert ids(response) == [100, 1, 2, 3]
assert response.data["data"][0]["organization"][field]
assert [row["organization"][field] for row in response.data["data"][1:]] == [
None,
None,
None,
]
@pytest.mark.parametrize("field", ["name", "full_name"])
@pytest.mark.parametrize("prefix", ["organization__", "extension__organization__"])
@pytest.mark.parametrize("descending", [False, True])
def test_sro_ordering_uses_displayed_names_before_pagination(
registry_query, field, prefix, descending
):
client, _ = registry_query
for uid, canonical, displayed in (
(101, "Z", "Alpha"),
(102, "A", "Zulu"),
(103, "Middle", ""),
(104, "", None),
(105, "Y", "Alpha"),
):
organization = Organization.objects.create(
name=canonical or "Temporary name", full_name=canonical
)
Organization.objects.filter(pk=organization.pk).update(**{field: canonical})
extension = SroMembershipExtension.objects.create(organization=organization)
OrganizationSourceRecord.objects.create(
uid=UUID(int=uid),
extension=extension,
source="sro_membership_check",
record_type="sro_membership",
external_id=str(uid),
payload={
"source_organization": {field: displayed},
"membership_status": "active",
"membership_status_raw": "active",
"region": "Москва",
"sro_id": str(uid),
"sro_name": "СРО",
"sro_url": f"https://reestr-sro.ru/sro/{uid}",
"admission_date": None,
"source_registry_date": None,
},
)
actual_ids, names = [], []
for page in (1, 2, 3):
response = get_records(
client,
source_group="sro_membership",
ordering=("-" if descending else "") + prefix + field,
page_size=2,
page=page,
)
actual_ids.extend(ids(response))
names.extend(row["organization"][field] for row in response.data["data"])
assert actual_ids == (
[102, 103, 101, 105, 104] if descending else [101, 105, 103, 102, 104]
)
assert names == (
["Zulu", "Middle", "Alpha", "Alpha", None]
if descending
else ["Alpha", "Alpha", "Middle", "Zulu", None]
)
@pytest.mark.parametrize(
("params", "code"),
[
({"organization": "not-a-uuid"}, "invalid_uuid"),
({"unknown_filter": "yes"}, "unknown_filter"),
({"ordering": "payload__secret"}, "invalid_ordering"),
({"is_branch": "perhaps"}, "invalid_boolean"),
],
)
def test_detail_rejects_invalid_query_with_typed_400(registry_query, params, code):
client, records = registry_query
response = client.get(
reverse(
"api_v2:organizations:organization-source-records-detail",
kwargs={"uid": records[0].uid},
),
params,
)
assert response.status_code == 400, response.data
assert response.data["success"] is False
assert response.data["data"] is None
assert response.data["errors"][0]["code"] == code
def test_missing_detail_returns_source_record_not_found(registry_query):
client, _ = registry_query
response = client.get(
reverse(
"api_v2:organizations:organization-source-records-detail",
kwargs={"uid": UUID(int=999)},
)
)
assert response.status_code == 404, response.data
assert response.data["data"] is None
assert response.data["errors"][0]["code"] == "source_record_not_found"
def test_sme_published_filter_includes_legacy_empty_status_only_for_sme(registry_query):
client, records = registry_query
OrganizationSourceRecord.objects.filter(uid=records[3].uid).update(
status="published"
)
OrganizationSourceRecord.objects.filter(uid=records[0].uid).update(status="")
response = get_records(client, status="published", ordering="uid")
assert ids(response) == [4, 5, 6]
assert {row["status"] for row in response.data["data"]} == {"published"}
def test_legacy_source_preserves_empty_text_ordering(registry_query):
client, records = registry_query
organization = Organization.objects.create(name="Other", full_name="Filled")
extension = BudgetProcessRegistryExtension.objects.create(organization=organization)
OrganizationSourceRecord.objects.create(
uid=UUID(int=100),
extension=extension,
source="legacy",
record_type="legacy",
external_id="filled",
payload={},
)
OrganizationSourceRecord.objects.filter(uid=records[0].uid).update(source="legacy")
response = get_records(client, source="legacy", ordering="organization__full_name")
assert ids(response) == [1, 100]
assert [row["organization"]["full_name"] for row in response.data["data"]] == [
"",
"Filled",
]