Files
mostovik-backend/tests/apps/organizations/test_api_v2_source_extensions.py
Aleksandr Meshchriakov cdc4698864
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 1m20s
CI/CD Pipeline / Build and Push Images (push) Successful in 17s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 31s
feat(api): add media news sorting
2026-08-18 10:27:15 +02:00

750 lines
30 KiB
Python
Raw 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.
"""Tests for organization API v2 backed by source extensions."""
from uuid import UUID
from django.core.cache import cache
from django.urls import reverse
from organizations.models import (
ArbitrationExtension,
ElectronicDocumentExchangeExtension,
MediaMentionExtension,
Organization,
OrganizationSourceRecord,
PlannedInspectionExtension,
VacancyExtension,
)
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.user.factories import UserFactory
def create_frontend_organization(**kwargs):
"""Create an organization visible through the frontend API by default."""
kwargs.setdefault("opk_registry_membership", True)
return Organization.objects.create(**kwargs)
class OrganizationSourceExtensionsApiV2Test(APITestCase):
"""Checks organization-centric source extension API contract."""
def setUp(self):
cache.clear()
self.user = UserFactory.create_user()
self.client.force_authenticate(self.user)
def test_list_returns_compact_source_summaries_instead_of_embedded_data(self):
organization = create_frontend_organization(
name='ООО "API"',
inn="7707083810",
ogrn="1027700132010",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
records_count=1,
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-API",
title="Проверка API",
payload={"registration_number": "INSP-API"},
)
response = self.client.get(
reverse("api_v2:organizations:organizations-list"),
{"has_registry": "false"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
item = response.data["data"][0]
self.assertNotIn("data", item)
self.assertNotIn("data_sources", item)
self.assertEqual(item["identity_status"], Organization.IdentityStatus.COMPLETE)
self.assertEqual(item["sources"][0]["uid"], str(extension.uid))
self.assertEqual(item["sources"][0]["source_group"], "planned_inspections")
self.assertEqual(item["sources"][0]["records_count"], 1)
def test_organization_sources_action_returns_extensions(self):
organization = create_frontend_organization(
name='ООО "Sources"',
inn="7707083811",
ogrn="1027700132011",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
response = self.client.get(
reverse(
"api_v2:organizations:organizations-sources",
args=[organization.uid],
)
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data[0]["uid"], str(extension.uid))
def test_source_records_endpoint_returns_extension_records(self):
organization = create_frontend_organization(
name='ООО "Records"',
inn="7707083812",
ogrn="1027700132012",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-RECORD",
title="Проверка records",
payload={"registration_number": "INSP-RECORD"},
)
response = self.client.get(
reverse(
"api_v2:organizations:organization-sources-records",
args=[extension.uid],
)
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["data"][0]["external_id"], "INSP-RECORD")
self.assertEqual(
response.data["data"][0]["payload"]["registration_number"],
"INSP-RECORD",
)
def test_flat_source_records_endpoint_filters_by_source_group(self):
target = create_frontend_organization(
name='ООО "Flat Records"',
inn="7707083813",
ogrn="1027700132013",
)
other = create_frontend_organization(
name='ООО "Other Records"',
inn="7707083814",
ogrn="1027700132014",
)
extension = PlannedInspectionExtension.objects.create(
organization=target,
title="Плановые проверки Генпрокуратуры России",
)
other_extension = PlannedInspectionExtension.objects.create(
organization=other,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-FLAT",
title="Проверка flat",
payload={"registration_number": "INSP-FLAT"},
)
OrganizationSourceRecord.objects.create(
extension=other_extension,
record_type="inspection",
source="inspections",
external_id="INSP-OTHER",
title="Другая проверка",
payload={"registration_number": "INSP-OTHER"},
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"source_group": "planned_inspections",
"organization": str(target.uid),
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 1)
record = response.data["data"][0]
self.assertEqual(record["external_id"], "INSP-FLAT")
self.assertEqual(record["source_group"], "planned_inspections")
self.assertEqual(record["organization"]["uid"], str(target.uid))
def test_flat_arbitration_records_expose_frontend_role_labels(self):
organization = create_frontend_organization(
name='ООО "Arbitration Roles"',
inn="7707083899",
ogrn="1027700132099",
)
extension = ArbitrationExtension.objects.create(
organization=organization,
title="Арбитраж",
)
expected_roles = {
"ROLE-PLAINTIFF": "Истец",
"ROLE-DEFENDANT": "Ответчик",
"ROLE-THIRD-PARTY": "Третье лицо",
}
for external_id, provider_role in (
("ROLE-PLAINTIFF", "plaintiff"),
("ROLE-DEFENDANT", "defendant"),
("ROLE-THIRD-PARTY", "third_party"),
):
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="arbitration_case",
source="arbitration",
external_id=external_id,
payload={"target": {"role": provider_role}},
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"source_group": "arbitration",
"source": "arbitration",
"organization": str(organization.uid),
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
records_by_external_id = {
item["external_id"]: item for item in response.data["data"]
}
self.assertEqual(
{
external_id: records_by_external_id[external_id]["payload"]["role"]
for external_id in expected_roles
},
expected_roles,
)
self.assertNotIn(
"role",
OrganizationSourceRecord.objects.get(external_id="ROLE-PLAINTIFF").payload,
)
def test_flat_source_records_filters_supported_groups_by_canonical_date(self):
organization = create_frontend_organization(
name='ООО "Canonical dates"',
inn="7707083890",
ogrn="1027700132090",
)
extensions = {
"planned_inspections": PlannedInspectionExtension.objects.create(
organization=organization,
title="Проверки",
),
"vacancies": VacancyExtension.objects.create(
organization=organization,
title="Вакансии",
),
"arbitration": ArbitrationExtension.objects.create(
organization=organization,
title="Арбитраж",
),
}
OrganizationSourceRecord.objects.create(
extension=extensions["planned_inspections"],
record_type="inspection",
source="inspections",
external_id="DATE-INSPECTION",
record_date="12.03.2026",
payload={"start_date_normalized": "2026-03-12"},
)
OrganizationSourceRecord.objects.create(
extension=extensions["vacancies"],
record_type="vacancy",
source="trudvsem",
external_id="DATE-VACANCY",
record_date="2026-03-31",
)
OrganizationSourceRecord.objects.create(
extension=extensions["arbitration"],
record_type="arbitration_case",
source="arbitration",
external_id="DATE-ARBITRATION",
record_date="2026-04-01",
)
OrganizationSourceRecord.objects.create(
extension=extensions["vacancies"],
record_type="vacancy",
source="trudvsem",
external_id="DATE-NULL",
record_date="",
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"date_from": "2026-03-12",
"date_to": "2026-03-31",
"ordering": "record_date",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 2)
self.assertEqual(
[item["external_id"] for item in response.data["data"]],
["DATE-INSPECTION", "DATE-VACANCY"],
)
self.assertEqual(
[item["record_date"] for item in response.data["data"]],
["2026-03-12", "2026-03-31"],
)
def test_flat_source_records_orders_dates_stably_with_nulls_last(self):
organization = create_frontend_organization(
name='ООО "Stable dates"',
inn="7707083891",
ogrn="1027700132091",
)
extension = VacancyExtension.objects.create(
organization=organization,
title="Вакансии",
)
records = [
("00000000-0000-0000-0000-000000000004", "DATE-NULL", ""),
("00000000-0000-0000-0000-000000000003", "DATE-LATE", "2026-05-01"),
("00000000-0000-0000-0000-000000000002", "DATE-EQUAL-2", "2026-04-01"),
("00000000-0000-0000-0000-000000000001", "DATE-EQUAL-1", "2026-04-01"),
]
for uid, external_id, record_date in records:
OrganizationSourceRecord.objects.create(
uid=UUID(uid),
extension=extension,
record_type="vacancy",
source="trudvsem",
external_id=external_id,
record_date=record_date,
)
endpoint = reverse("api_v2:organizations:organization-source-records-list")
ascending = self.client.get(endpoint, {"ordering": "record_date"})
descending = self.client.get(endpoint, {"ordering": "-record_date"})
self.assertEqual(ascending.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["external_id"] for item in ascending.data["data"]],
["DATE-EQUAL-1", "DATE-EQUAL-2", "DATE-LATE", "DATE-NULL"],
)
self.assertEqual(
[item["external_id"] for item in descending.data["data"]],
["DATE-LATE", "DATE-EQUAL-1", "DATE-EQUAL-2", "DATE-NULL"],
)
self.assertIsNone(ascending.data["data"][-1]["record_date"])
self.assertIsNone(descending.data["data"][-1]["record_date"])
def test_flat_gosedo_records_support_frontend_sorting_fields(self):
records = []
for external_id, name, full_name, attestation_status in (
("GOSEDO-BETA", "Организация Бета", "Полное имя Бета", "not_attested"),
("GOSEDO-ALPHA", "Организация Альфа", "Полное имя Альфа", "attested"),
):
organization = create_frontend_organization(
name=name,
full_name=full_name,
inn="7707083815" if external_id == "GOSEDO-BETA" else "7707083816",
ogrn=(
"1027700132015" if external_id == "GOSEDO-BETA" else "1027700132016"
),
)
extension = ElectronicDocumentExchangeExtension.objects.create(
organization=organization,
title="Электронный документооборот",
)
records.append(
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="participant",
source="gosedo_address_directory",
external_id=external_id,
payload={"attestation_status": attestation_status},
)
)
endpoint = reverse("api_v2:organizations:organization-source-records-list")
params = {
"source_group": "electronic_document_exchange",
"source": "gosedo_address_directory",
"record_type": "participant",
}
expected_ascending = [records[1].external_id, records[0].external_id]
expected_descending = list(reversed(expected_ascending))
for ordering, expected in (
("extension__organization__full_name", expected_ascending),
("-extension__organization__full_name", expected_descending),
("payload__attestation_status", expected_ascending),
("-payload__attestation_status", expected_descending),
):
with self.subTest(ordering=ordering):
response = self.client.get(endpoint, {**params, "ordering": ordering})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["external_id"] for item in response.data["data"]],
expected,
)
def test_flat_media_records_support_frontend_sorting_fields(self):
organization = create_frontend_organization(
name="Media sorting organization",
inn="7707083817",
ogrn="1027700132017",
)
extension = MediaMentionExtension.objects.create(
organization=organization,
title="Media mentions",
)
for external_id, news_source, sentiment in (
("NEWS-ZULU", "Zulu Media", "negative"),
("NEWS-ALPHA", "Alpha Media", "positive"),
):
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="media_mention",
source="media_news",
external_id=external_id,
status=sentiment,
payload={
"news_source": news_source,
"sentiment": sentiment,
},
)
endpoint = reverse("api_v2:organizations:organization-source-records-list")
params = {
"source_group": "media_mentions",
"source": "media_news",
"record_type": "media_mention",
}
for ordering, expected in (
("payload__news_source", ["NEWS-ALPHA", "NEWS-ZULU"]),
("-payload__news_source", ["NEWS-ZULU", "NEWS-ALPHA"]),
("payload__sentiment", ["NEWS-ZULU", "NEWS-ALPHA"]),
("-payload__sentiment", ["NEWS-ALPHA", "NEWS-ZULU"]),
):
with self.subTest(ordering=ordering):
response = self.client.get(endpoint, {**params, "ordering": ordering})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["external_id"] for item in response.data["data"]],
expected,
)
def test_flat_source_records_rejects_invalid_date_range_and_ordering(self):
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"date_from": "2026-04-01",
"date_to": "2026-03-01",
"ordering": "unknown_date",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data["success"])
self.assertEqual(response.data["data"], [])
self.assertEqual(
{error["code"] for error in response.data["errors"]},
{"invalid_date_range", "invalid_ordering"},
)
def test_flat_source_records_rejects_invalid_date_format(self):
for invalid_date in ("01.04.2026", "2026-4-1"):
with self.subTest(invalid_date=invalid_date):
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{"date_from": invalid_date},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["errors"][0]["code"], "invalid_date")
self.assertEqual(response.data["errors"][0]["field"], "date_from")
self.assertIn("YYYY-MM-DD", response.data["errors"][0]["message"])
def test_flat_source_record_date_filter_precedes_pagination(self):
organization = create_frontend_organization(
name='ООО "Filtered pagination"',
inn="7707083892",
ogrn="1027700132092",
)
extension = ArbitrationExtension.objects.create(
organization=organization,
title="Арбитраж",
)
for index, record_date in enumerate(
("2026-01-01", "2026-02-01", "2026-03-01"),
start=1,
):
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="arbitration_case",
source="arbitration",
external_id=f"PAGE-{index}",
title="Искомое дело",
record_date=record_date,
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"source_group": "arbitration",
"source": "arbitration",
"organization": str(organization.uid),
"search": "Искомое",
"date_from": "2026-02-01",
"ordering": "record_date",
"page_size": 1,
"page": 2,
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 2)
self.assertEqual(response.data["meta"]["pagination"]["total_pages"], 2)
self.assertEqual(response.data["data"][0]["external_id"], "PAGE-3")
def test_flat_source_records_scope_cannot_be_lifted_by_has_registry(self):
with_registry = create_frontend_organization(
name='ООО "With Registry Source"',
inn="7707083815",
ogrn="1027700132015",
opk_registry_membership=True,
ropk_razdel_num="source-records",
)
without_registry = create_frontend_organization(
name='ООО "Without Registry Source"',
inn="7707083816",
ogrn="1027700132016",
opk_registry_membership=False,
)
with_registry_extension = PlannedInspectionExtension.objects.create(
organization=with_registry,
title="Плановые проверки Генпрокуратуры России",
)
without_registry_extension = PlannedInspectionExtension.objects.create(
organization=without_registry,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=with_registry_extension,
record_type="inspection",
source="inspections",
external_id="INSP-WITH-REGISTRY",
title="Проверка организации из реестра",
)
OrganizationSourceRecord.objects.create(
extension=without_registry_extension,
record_type="inspection",
source="inspections",
external_id="INSP-WITHOUT-REGISTRY",
title="Проверка организации без реестра",
)
only_registry = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "true",
"source_group": "planned_inspections",
},
)
without_registry_response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "false",
"source_group": "planned_inspections",
"ordering": "extension__organization__inn",
},
)
self.assertEqual(only_registry.status_code, status.HTTP_200_OK)
self.assertEqual(only_registry.data["meta"]["pagination"]["total_count"], 1)
self.assertEqual(
only_registry.data["data"][0]["external_id"],
"INSP-WITH-REGISTRY",
)
self.assertEqual(without_registry_response.status_code, status.HTTP_200_OK)
self.assertEqual(
without_registry_response.data["meta"]["pagination"]["total_count"],
1,
)
self.assertEqual(
without_registry_response.data["data"][0]["external_id"],
"INSP-WITH-REGISTRY",
)
def test_source_record_organization_uses_active_registry_identity(self):
organization = create_frontend_organization(
name='ООО "Реестровое имя"',
inn="1800020960",
kpp="180001001",
ogrn="1241800009703",
opk_registry_membership=True,
ropk_razdel_num="identity",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-REGISTRY-IDENTITY",
title="Проверка организации из реестра",
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "true",
"source_group": "planned_inspections",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 1)
response_organization = response.data["data"][0]["organization"]
self.assertEqual(response_organization["uid"], str(organization.uid))
self.assertEqual(response_organization["name"], 'ООО "Реестровое имя"')
self.assertEqual(response_organization["inn"], "1800020960")
self.assertEqual(response_organization["kpp"], "180001001")
self.assertEqual(response_organization["ogrn"], organization.ogrn)
def test_source_record_organization_keeps_canonical_inn_with_leading_zero(self):
organization = create_frontend_organization(
name='ООО "Башнефть-Добыча"',
inn="0277106840",
ogrn="1090280032699",
opk_registry_membership=True,
ropk_razdel_num="leading-zero",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-LEADING-ZERO-INN",
title="Проверка организации с ведущим нулем",
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "true",
"source_group": "planned_inspections",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_organization = response.data["data"][0]["organization"]
self.assertEqual(response_organization["inn"], "0277106840")
self.assertEqual(response_organization["ogrn"], organization.ogrn)
def test_flat_source_records_searches_payload_values_displayed_in_tables(self):
target = create_frontend_organization(
name='ООО "Поиск по payload"',
inn="7707083817",
ogrn="1027700132017",
)
other = create_frontend_organization(
name='ООО "Другая payload"',
inn="7707083818",
ogrn="1027700132018",
)
extension = PlannedInspectionExtension.objects.create(
organization=target,
title="Плановые проверки Генпрокуратуры России",
)
other_extension = PlannedInspectionExtension.objects.create(
organization=other,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-PAYLOAD",
title="Проверка payload",
payload={
"registration_number": "INSPECTION-PAYLOAD-42",
"control_authority": "Северный Ростехнадзор",
"start_date_normalized": "2026-06-15",
},
)
OrganizationSourceRecord.objects.create(
extension=other_extension,
record_type="inspection",
source="inspections",
external_id="INSP-OTHER-PAYLOAD",
title="Другая проверка payload",
payload={
"registration_number": "INSPECTION-OTHER-42",
"control_authority": "Другой контрольный орган",
},
)
for search_value in (
"INSPECTION-PAYLOAD-42",
"Северный Ростехнадзор",
"2026-06-15",
):
with self.subTest(search_value=search_value):
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "false",
"source_group": "planned_inspections",
"search": search_value,
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 1)
self.assertEqual(
response.data["data"][0]["external_id"],
"INSP-PAYLOAD",
)
def test_flat_source_records_searches_registry_identity_displayed_in_tables(self):
organization = create_frontend_organization(
name='АО "Реестровый поиск"',
inn="1800020961",
kpp="180001002",
ogrn="1241800009704",
opk_registry_membership=True,
ropk_razdel_num="search",
)
extension = PlannedInspectionExtension.objects.create(
organization=organization,
title="Плановые проверки Генпрокуратуры России",
)
OrganizationSourceRecord.objects.create(
extension=extension,
record_type="inspection",
source="inspections",
external_id="INSP-REGISTRY-SEARCH",
title="Проверка поиска по реестру",
)
response = self.client.get(
reverse("api_v2:organizations:organization-source-records-list"),
{
"has_registry": "true",
"source_group": "planned_inspections",
"search": "Реестровый поиск",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 1)
record = response.data["data"][0]
self.assertEqual(record["external_id"], "INSP-REGISTRY-SEARCH")
self.assertEqual(record["organization"]["name"], 'АО "Реестровый поиск"')