Files
mostovik-backend/tests/apps/parsers/test_views.py
Aleksandr Meshchriakov 0f17ff6773
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 26s
CI/CD Pipeline / Build and Push Images (push) Successful in 6s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s
Add organizations v2 API and registry enrichment
2026-05-06 19:04:46 +02:00

891 lines
35 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.
"""Integration tests for parsers API views (no mocks)."""
from __future__ import annotations
import io
import os
import tempfile
import zipfile
from unittest.mock import Mock, patch
from apps.parsers.models import (
FinancialReport,
FinancialReportLine,
GenericParserRecord,
ParserLoadLog,
ProcurementRecord,
)
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from openpyxl import Workbook
from organizations.models import Organization
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.parsers.factories import (
IndustrialCertificateRecordFactory,
IndustrialProductRecordFactory,
InspectionRecordFactory,
ManufacturerRecordFactory,
ParserLoadLogFactory,
)
from tests.apps.registers.factories import (
OrganizationFactory as RegisterOrganizationFactory,
)
from tests.apps.registers.factories import (
RegistryMembershipPeriodFactory,
)
from tests.apps.user.factories import UserFactory
from tests.utils.fixtures import fake
def _digits(length: int) -> str:
return "".join(str(fake.random_int(0, 9)) for _ in range(length))
def _build_fns_excel_bytes() -> bytes:
wb = Workbook()
ws = wb.active
year = fake.random_int(min=2020, max=2025)
ws.append(["Form", None, year, None])
ws.append([None, "Code", "Start", "End"])
ws.append(
[fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)]
)
buf = io.BytesIO()
wb.save(buf)
wb.close()
return buf.getvalue()
def _build_fns_zip_bytes(file_map: dict[str, bytes]) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for file_name, content in file_map.items():
archive.writestr(file_name, content)
return buf.getvalue()
def _create_procurement_record() -> ProcurementRecord:
return ProcurementRecord.objects.create(
load_batch=fake.random_int(min=1, max=1000),
purchase_number=_digits(19),
purchase_name=fake.sentence(nb_words=6),
customer_inn=_digits(10),
customer_kpp=_digits(9),
customer_ogrn=_digits(13),
customer_name=fake.company(),
max_price=str(fake.pydecimal(left_digits=7, right_digits=2, positive=True)),
status=fake.word(),
law_type="44-FZ",
href=fake.url(),
region_code=f"{fake.random_int(min=1, max=99):02d}",
)
class ParsersViewSetTest(APITestCase):
def setUp(self):
self.user = UserFactory.create_user()
self.admin = UserFactory.create_superuser()
def test_certificates_list_and_retrieve(self):
record = IndustrialCertificateRecordFactory()
self.client.force_authenticate(self.user)
url = reverse("api_v1:minpromtorg:certificates-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:minpromtorg:certificates-detail", args=[record.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_manufacturers_list_and_retrieve(self):
record = ManufacturerRecordFactory()
second_record = ManufacturerRecordFactory()
self.client.force_authenticate(self.user)
url = reverse("api_v1:minpromtorg:manufacturers-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:minpromtorg:manufacturers-detail", args=[record.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
first_page_response = self.client.get(
"/api/v1/parsers/results/manufactures/",
{"page": 1, "page_size": 1},
)
second_page_response = self.client.get(
"/api/v1/parsers/results/manufactures/",
{"page": 2, "page_size": 1},
)
self.assertEqual(first_page_response.status_code, status.HTTP_200_OK)
self.assertEqual(second_page_response.status_code, status.HTTP_200_OK)
self.assertEqual(first_page_response.data["meta"]["pagination"]["count"], 2)
self.assertEqual(second_page_response.data["meta"]["pagination"]["count"], 2)
self.assertNotEqual(
first_page_response.data["data"][0]["id"],
second_page_response.data["data"][0]["id"],
)
self.assertEqual(
{
first_page_response.data["data"][0]["id"],
second_page_response.data["data"][0]["id"],
},
{record.id, second_record.id},
)
def test_products_list_and_retrieve(self):
record = IndustrialProductRecordFactory()
self.client.force_authenticate(self.user)
url = reverse("api_v1:minpromtorg:industrial-products-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:minpromtorg:industrial-products-detail", args=[record.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_inspections_list_and_retrieve(self):
record = InspectionRecordFactory()
self.client.force_authenticate(self.user)
url = reverse("api_v1:proverki:inspections-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:proverki:inspections-detail", args=[record.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_procurements_list_and_retrieve(self):
record = _create_procurement_record()
self.client.force_authenticate(self.user)
url = reverse("api_v1:zakupki:procurements-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_procurements_v1_enriches_missing_customer_fields_from_canonical_organization(
self,
):
organization = Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
inn="7701000101",
kpp="770101001",
ogrn="1027700000001",
)
record = ProcurementRecord.objects.create(
load_batch=1,
purchase_number="0320100000126000001",
purchase_name="Поставка оборудования",
customer_inn="",
customer_kpp="",
customer_ogrn=organization.ogrn,
customer_name="",
max_price="1000.00",
status="published",
law_type="44-FZ",
href="https://zakupki.gov.ru/example",
region_code="77",
)
self.client.force_authenticate(self.user)
list_response = self.client.get(reverse("api_v1:zakupki:procurements-list"))
detail_response = self.client.get(
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
list_row = list_response.data["data"][0]
detail_row = detail_response.data
self.assertEqual(list_row["customer_name"], organization.name)
self.assertEqual(list_row["customer_inn"], organization.inn)
self.assertEqual(list_row["customer_kpp"], organization.kpp)
self.assertEqual(list_row["customer_ogrn"], organization.ogrn)
self.assertEqual(detail_row["customer_name"], organization.name)
self.assertEqual(detail_row["customer_inn"], organization.inn)
self.assertEqual(detail_row["customer_kpp"], organization.kpp)
self.assertEqual(detail_row["customer_ogrn"], organization.ogrn)
def test_procurements_v1_uses_customer_kpp_for_branch_enrichment(self):
head = Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
inn="7701000101",
kpp="770101001",
ogrn="1027700000001",
)
branch = Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ',
inn=head.inn,
kpp="780101001",
ogrn=head.ogrn,
)
record = ProcurementRecord.objects.create(
load_batch=1,
purchase_number="0320100000126000002",
purchase_name="Поставка оборудования",
customer_inn=head.inn,
customer_kpp=branch.kpp,
customer_ogrn=head.ogrn,
customer_name="",
max_price="1000.00",
status="published",
law_type="44-FZ",
href="https://zakupki.gov.ru/example",
region_code="77",
)
self.client.force_authenticate(self.user)
response = self.client.get(
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["customer_name"], branch.name)
self.assertEqual(response.data["customer_kpp"], branch.kpp)
def test_procurements_v1_does_not_guess_branch_when_customer_kpp_is_missing(self):
head = Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"',
inn="7701000101",
kpp="770101001",
ogrn="1027700000001",
)
Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ',
inn=head.inn,
kpp="780101001",
ogrn=head.ogrn,
)
record = ProcurementRecord.objects.create(
load_batch=1,
purchase_number="0320100000126000003",
purchase_name="Поставка оборудования",
customer_inn=head.inn,
customer_kpp="",
customer_ogrn=head.ogrn,
customer_name="",
max_price="1000.00",
status="published",
law_type="44-FZ",
href="https://zakupki.gov.ru/example",
region_code="77",
)
self.client.force_authenticate(self.user)
response = self.client.get(
reverse("api_v1:zakupki:procurements-detail", args=[record.id])
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["customer_name"], "")
self.assertEqual(response.data["customer_kpp"], "")
def test_eis_result_endpoint_uses_generic_records_without_breaking_old_zakupki_api(
self,
):
old_record = _create_procurement_record()
generic_record = GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
external_id="eis-44fz-1",
inn=_digits(10),
organisation_name="EIS Customer",
title="EIS 44-FZ notice",
status="published",
payload={"registry": "44fz"},
)
self.client.force_authenticate(self.user)
old_response = self.client.get(reverse("api_v1:zakupki:procurements-list"))
new_response = self.client.get("/api/v1/eis/procurements-44fz/")
detail_response = self.client.get(
f"/api/v1/eis/procurements-44fz/{generic_record.id}/"
)
self.assertEqual(old_response.status_code, status.HTTP_200_OK)
self.assertEqual(old_response.data["data"][0]["id"], old_record.id)
self.assertEqual(new_response.status_code, status.HTTP_200_OK)
self.assertEqual(new_response.data["meta"]["pagination"]["count"], 1)
self.assertEqual(new_response.data["data"][0]["id"], generic_record.id)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["data"]["payload"], {"registry": "44fz"})
def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change(
self,
):
organization = Organization.objects.create(
name='ООО "КАНОНИЧЕСКИЙ ПОСТАВЩИК"',
inn="7701000102",
kpp="770102001",
ogrn="1027700000002",
)
generic_record = GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
external_id="eis-44fz-enrichment",
inn="",
ogrn=organization.ogrn,
organisation_name="",
title="EIS notice",
status="published",
payload={"registry": "44fz"},
)
self.client.force_authenticate(self.user)
response = self.client.get("/api/v1/parsers/results/procurements_44fz/")
detail_response = self.client.get(
f"/api/v1/parsers/results/procurements_44fz/{generic_record.id}/"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
row = response.data["data"][0]
detail = detail_response.data["data"]
self.assertEqual(row["organisation_name"], organization.name)
self.assertEqual(row["inn"], organization.inn)
self.assertEqual(row["ogrn"], organization.ogrn)
self.assertNotIn("kpp", row)
self.assertEqual(detail["organisation_name"], organization.name)
self.assertEqual(detail["inn"], organization.inn)
self.assertEqual(detail["ogrn"], organization.ogrn)
self.assertNotIn("kpp", detail)
def test_dashboard_data_exposes_source_groups_for_page(self):
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
external_id="eis-44fz-1",
title="EIS 44-FZ notice 1",
payload={"registry": "44fz"},
)
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
external_id="eis-44fz-2",
title="EIS 44-FZ notice 2",
payload={"registry": "44fz"},
)
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.TRUDVSEM,
external_id="trudvsem-1",
title="Vacancy",
payload={"registry": "trudvsem"},
)
FinancialReport.objects.create(
external_id=_digits(5),
ogrn=_digits(13),
file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx",
file_hash=fake.sha256(raw_output=False),
load_batch=1,
status=FinancialReport.Status.SUCCESS,
source=FinancialReport.SourceType.API,
)
self.client.force_authenticate(self.user)
response = self.client.get("/api/v1/parsers/dashboard/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
payload = response.data["data"]
self.assertIn("groups", payload)
self.assertIn("api", payload["groups"])
self.assertIn("uploads", payload["groups"])
self.assertEqual(payload["api_sources"], payload["groups"]["api"])
self.assertEqual(payload["file_sources"], payload["groups"]["uploads"])
sources = {item["key"]: item for item in payload["sources"]}
self.assertEqual(
sources["procurements_44fz"]["result_list_url"],
"/api/v1/parsers/results/procurements_44fz/",
)
self.assertEqual(
sources["procurements_223fz"]["result_list_url"],
"/api/v1/parsers/results/procurements_223fz/",
)
self.assertEqual(
sources["contracts"]["result_list_url"],
"/api/v1/parsers/results/contracts/",
)
self.assertEqual(
payload["source_counts"][ParserLoadLog.Source.PROCUREMENTS_44FZ],
2,
)
self.assertEqual(payload["source_counts"][ParserLoadLog.Source.TRUDVSEM], 1)
self.assertEqual(payload["source_counts"][ParserLoadLog.Source.FNS_REPORTS], 1)
def test_dashboard_data_exposes_registry_data_coverage(self):
registry_organization = RegisterOrganizationFactory(
mn_inn=7720699480,
mn_ogrn=1107746880031,
)
RegistryMembershipPeriodFactory(organization=registry_organization)
FinancialReport.objects.create(
external_id=_digits(5),
ogrn=str(registry_organization.mn_ogrn),
file_name=f"fin_{_digits(5)}_{registry_organization.mn_ogrn}.xlsx",
file_hash=fake.sha256(raw_output=False),
load_batch=1,
status=FinancialReport.Status.SUCCESS,
source=FinancialReport.SourceType.API,
)
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
external_id="unfair-1",
title="Unfair supplier record",
inn=str(registry_organization.mn_inn),
ogrn=str(registry_organization.mn_ogrn),
)
self.client.force_authenticate(self.user)
response = self.client.get("/api/v1/parsers/dashboard/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
coverage = response.data["data"]["registry_data_coverage"]
self.assertEqual(coverage["total_organizations"], 1)
items = {item["source"]: item for item in coverage["items"]}
self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["organizations_count"], 1)
self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["coverage_percent"], 100)
self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, items)
def test_dashboard_data_exposes_registry_enrichment_analytics(self):
rosatom_organization = RegisterOrganizationFactory(
mn_inn=7720699480,
mn_ogrn=1107746880031,
)
roscosmos_organization = RegisterOrganizationFactory(
mn_inn=7730239877,
mn_ogrn=1087302001797,
)
rosatom_membership = RegistryMembershipPeriodFactory(
organization=rosatom_organization
)
roscosmos_membership = RegistryMembershipPeriodFactory(
organization=roscosmos_organization
)
FinancialReport.objects.create(
external_id=_digits(5),
ogrn=str(rosatom_organization.mn_ogrn),
file_name=f"fin_{_digits(5)}_{rosatom_organization.mn_ogrn}.xlsx",
file_hash=fake.sha256(raw_output=False),
load_batch=1,
status=FinancialReport.Status.SUCCESS,
source=FinancialReport.SourceType.API,
)
IndustrialCertificateRecordFactory(
inn=str(rosatom_organization.mn_inn),
ogrn=str(rosatom_organization.mn_ogrn),
)
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.UNFAIR_SUPPLIERS,
external_id="unfair-registry-analytics",
title="Risk signal",
inn=str(roscosmos_organization.mn_inn),
ogrn=str(roscosmos_organization.mn_ogrn),
)
GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY,
external_id="bankruptcy-registry-analytics",
title="Bankruptcy risk signal",
inn=str(roscosmos_organization.mn_inn),
ogrn=str(roscosmos_organization.mn_ogrn),
)
InspectionRecordFactory(
inn=str(rosatom_organization.mn_inn),
ogrn=str(rosatom_organization.mn_ogrn),
)
self.client.force_authenticate(self.user)
response = self.client.get("/api/v1/parsers/dashboard/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
analytics = response.data["data"]["registry_enrichment_analytics"]
self.assertEqual(
analytics["population"]["active_registry_organizations"],
2,
)
self.assertEqual(analytics["coverage_summary"]["with_any_enrichment"], 1)
self.assertEqual(analytics["coverage_summary"]["core_profile_complete"], 1)
self.assertEqual(analytics["coverage_summary"]["requires_attention"], 1)
source_coverage = {
item["source"]: item for item in analytics["source_coverage"]
}
self.assertEqual(
source_coverage[ParserLoadLog.Source.FNS_REPORTS][
"organizations_count"
],
1,
)
self.assertEqual(
source_coverage[ParserLoadLog.Source.INDUSTRIAL][
"organizations_count"
],
1,
)
self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, source_coverage)
self.assertNotIn(ParserLoadLog.Source.FEDRESURS_BANKRUPTCY, source_coverage)
self.assertNotIn(ParserLoadLog.Source.INSPECTIONS, source_coverage)
risk_signals = {item["source"]: item for item in analytics["risk_signals"]}
self.assertEqual(
risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS][
"organizations_count"
],
1,
)
self.assertEqual(
risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS]["risk_severity"],
"critical",
)
self.assertEqual(
risk_signals[ParserLoadLog.Source.FEDRESURS_BANKRUPTCY][
"risk_severity"
],
"critical",
)
self.assertEqual(
risk_signals[ParserLoadLog.Source.INSPECTIONS]["risk_severity"],
"soft",
)
self.assertEqual(
analytics["risk_summary"]["critical"]["organizations_count"],
1,
)
self.assertEqual(
analytics["risk_summary"]["soft"]["organizations_count"],
1,
)
matrix = {
item["registry_id"]: item
for item in analytics["registry_source_matrix"]
}
rosatom_matrix = matrix[str(rosatom_membership.registry_id)]
roscosmos_matrix = matrix[str(roscosmos_membership.registry_id)]
self.assertEqual(rosatom_matrix["active_organizations"], 1)
self.assertEqual(
rosatom_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
"organizations_count"
],
1,
)
self.assertEqual(
roscosmos_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
"organizations_count"
],
0,
)
total_matrix = analytics["registry_source_matrix"][-1]
self.assertEqual(total_matrix["registry_id"], "__all__")
self.assertEqual(total_matrix["registry_name"], "Все организации")
self.assertEqual(total_matrix["active_organizations"], 2)
self.assertEqual(
total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
"organizations_count"
],
1,
)
self.assertEqual(
total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][
"coverage_percent"
],
50,
)
self.assertEqual(
total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][
"organizations_count"
],
1,
)
self.assertEqual(
total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][
"coverage_percent"
],
50,
)
def test_financial_reports_list_and_retrieve(self):
report = FinancialReport.objects.create(
external_id=_digits(5),
ogrn=_digits(13),
file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx",
file_hash=fake.sha256(raw_output=False),
load_batch=fake.random_int(min=1, max=1000),
status=FinancialReport.Status.SUCCESS,
source=FinancialReport.SourceType.API,
)
FinancialReportLine.objects.create(
report=report,
form_code="1",
line_code=_digits(4),
line_name=fake.word(),
year=fake.random_int(min=2020, max=2025),
period_start=fake.random_int(min=1, max=999),
period_end=fake.random_int(min=1, max=999),
)
self.client.force_authenticate(self.user)
url = reverse("api_v1:fns:fns-reports-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["data"][0]["lines_count"], 1)
alias_response = self.client.get(url, {"source": "fns_reports"})
self.assertEqual(alias_response.status_code, status.HTTP_200_OK)
self.assertEqual(alias_response.data["data"][0]["id"], report.id)
source_type_response = self.client.get(
url,
{"source": FinancialReport.SourceType.API},
)
self.assertEqual(source_type_response.status_code, status.HTTP_200_OK)
self.assertEqual(source_type_response.data["data"][0]["id"], report.id)
invalid_source_response = self.client.get(url, {"source": "unknown"})
self.assertEqual(invalid_source_response.status_code, status.HTTP_200_OK)
self.assertEqual(invalid_source_response.data["data"], [])
detail = self.client.get(
reverse("api_v1:fns:fns-reports-detail", args=[report.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
self.assertIn("lines", detail.data)
unified_response = self.client.get("/api/v1/parsers/results/fns_financial/")
self.assertEqual(unified_response.status_code, status.HTTP_200_OK)
self.assertEqual(unified_response.data["meta"]["pagination"]["count"], 1)
self.assertEqual(unified_response.data["data"][0]["id"], report.id)
self.assertEqual(unified_response.data["data"][0]["title"], report.file_name)
def test_system_logs_admin_only(self):
log = ParserLoadLogFactory()
url_logs = reverse("api_v1:system:parser-logs-list")
response = self.client.get(url_logs)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(self.user)
response = self.client.get(url_logs)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.force_authenticate(self.admin)
response = self.client.get(url_logs)
self.assertEqual(response.status_code, status.HTTP_200_OK)
detail = self.client.get(
reverse("api_v1:system:parser-logs-detail", args=[log.id])
)
self.assertEqual(detail.status_code, status.HTTP_200_OK)
def test_system_logs_support_search_and_organizations_count(self):
search_marker = "manufactures-unique-search-marker"
first_log = ParserLoadLogFactory(
source="manufactures",
batch_id=101,
status="success",
error_message=search_marker,
)
ParserLoadLogFactory(
source="inspections",
batch_id=202,
status="failed",
error_message="timeout",
)
ManufacturerRecordFactory(load_batch=101, inn="7701000001")
ManufacturerRecordFactory(load_batch=101, inn="7701000002")
self.client.force_authenticate(self.admin)
response = self.client.get(
reverse("api_v1:system:parser-logs-list"),
{"search": search_marker},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
rows = response.data["results"]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["id"], first_log.id)
self.assertEqual(rows[0]["organizations_count"], 2)
def test_system_logs_export_returns_csv(self):
ParserLoadLogFactory(
source="manufactures",
batch_id=333,
status="success",
records_count=4,
)
ManufacturerRecordFactory(load_batch=333, inn="7701000001")
self.client.force_authenticate(self.admin)
response = self.client.get(
reverse("api_v1:system:parser-logs-export"),
{"search": "333"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8")
content = response.content.decode("utf-8")
self.assertIn("organizations_count", content)
self.assertIn("333", content)
def test_system_logs_support_search_by_source_label(self):
ParserLoadLogFactory(
source="fns_reports",
batch_id=909,
status="success",
)
self.client.force_authenticate(self.admin)
response = self.client.get(
reverse("api_v1:system:parser-logs-list"),
{"search": "финансово"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
self.assertEqual(response.data["results"][0]["source"], "financial-indicators")
def test_fns_upload_invalid_filename(self):
self.client.force_authenticate(self.admin)
with tempfile.TemporaryDirectory() as tmpdir:
watch_dir = os.path.join(tmpdir, "watch")
processed_dir = os.path.join(tmpdir, "processed")
failed_dir = os.path.join(tmpdir, "failed")
content = _build_fns_excel_bytes()
upload = SimpleUploadedFile(
f"bad_{fake.random_int()}.xlsx",
content,
content_type=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
)
with self.settings(
FNS_WATCH_DIRECTORY=watch_dir,
FNS_PROCESSED_DIRECTORY=processed_dir,
FNS_FAILED_DIRECTORY=failed_dir,
):
url = reverse("api_v1:fns:fns-upload")
response = self.client.post(
url, {"files": [upload]}, format="multipart"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_fns_upload_forbidden_for_regular_user(self):
self.client.force_authenticate(self.user)
with tempfile.TemporaryDirectory() as tmpdir:
watch_dir = os.path.join(tmpdir, "watch")
processed_dir = os.path.join(tmpdir, "processed")
failed_dir = os.path.join(tmpdir, "failed")
content = _build_fns_excel_bytes()
upload = SimpleUploadedFile(
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
content,
content_type=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
)
with self.settings(
FNS_WATCH_DIRECTORY=watch_dir,
FNS_PROCESSED_DIRECTORY=processed_dir,
FNS_FAILED_DIRECTORY=failed_dir,
):
url = reverse("api_v1:fns:fns-upload")
response = self.client.post(
url, {"files": [upload]}, format="multipart"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_fns_upload_accepts_single_file_payload(self):
self.client.force_authenticate(self.admin)
with tempfile.TemporaryDirectory() as tmpdir:
watch_dir = os.path.join(tmpdir, "watch")
processed_dir = os.path.join(tmpdir, "processed")
failed_dir = os.path.join(tmpdir, "failed")
content = _build_fns_excel_bytes()
upload = SimpleUploadedFile(
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
content,
content_type=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
)
with self.settings(
FNS_WATCH_DIRECTORY=watch_dir,
FNS_PROCESSED_DIRECTORY=processed_dir,
FNS_FAILED_DIRECTORY=failed_dir,
):
url = reverse("api_v1:fns:fns-upload")
response = self.client.post(url, {"file": upload}, format="multipart")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["success"], True)
self.assertIn("message", response.data)
def test_fns_upload_accepts_zip_payload(self):
self.client.force_authenticate(self.admin)
external_id = _digits(5)
ogrn = _digits(13)
filename = f"fin_{external_id}_{ogrn}.xlsx"
upload = SimpleUploadedFile(
"fns_reports.zip",
_build_fns_zip_bytes({filename: _build_fns_excel_bytes()}),
content_type="application/zip",
)
with tempfile.TemporaryDirectory() as tmpdir:
watch_dir = os.path.join(tmpdir, "watch")
processed_dir = os.path.join(tmpdir, "processed")
failed_dir = os.path.join(tmpdir, "failed")
with self.settings(
FNS_WATCH_DIRECTORY=watch_dir,
FNS_PROCESSED_DIRECTORY=processed_dir,
FNS_FAILED_DIRECTORY=failed_dir,
):
url = reverse("api_v1:fns:fns-upload")
response = self.client.post(url, {"file": upload}, format="multipart")
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["queued"], 1)
self.assertEqual(response.data["skipped"], 0)
self.assertEqual(response.data["invalid"], 0)
self.assertEqual(FinancialReport.objects.count(), 1)
def test_parsing_settings_get_and_patch(self):
self.client.force_authenticate(self.admin)
url = reverse("api_v1:parsing:parsing-settings")
initial = self.client.get(url)
self.assertEqual(initial.status_code, status.HTTP_200_OK)
self.assertEqual(initial.data["planned_inspections"], "monthly")
updated = self.client.patch(
url,
{"planned_inspections": "weekly"},
format="json",
)
self.assertEqual(updated.status_code, status.HTTP_200_OK)
self.assertEqual(updated.data["planned_inspections"], "weekly")
def test_run_sync_inspections_accepts_limited_sync_params(self):
self.client.force_authenticate(self.user)
url = reverse("api_v1:parsers:run-parser", args=["sync_inspections"])
payload = {
"max_months_per_law": 1,
"start_year": 2026,
"start_month": 4,
"include_fz294": True,
"include_fz248": False,
"current_year": 2026,
"current_month": 4,
}
with patch(
"apps.parsers.views.tasks.sync_inspections.apply_async",
return_value=Mock(id="task-123"),
) as apply_async_mock:
response = self.client.post(url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
task_kwargs = apply_async_mock.call_args.kwargs["kwargs"]
for key, value in payload.items():
self.assertEqual(task_kwargs[key], value)
self.assertEqual(task_kwargs["requested_by_id"], self.user.id)